[manual roll] Manually update 3p packages. Run the following command: $ scripts/dart/update_3p_packages.py --debug --flutter-revision 82735b8904e82fd8b273cb1ae16cccd77ccf4248 See https://fxrev.dev/615200. Change-Id: I17eacbbae5e3677216cc66f1849f5604b8e43e83 Reviewed-on: https://fuchsia-review.googlesource.com/c/third_party/dart-pkg/+/615309 Fuchsia-Auto-Submit: Darren Chan <chandarren@google.com> Reviewed-by: Sijie Chen <sijiec@google.com> Reviewed-by: Chase Latta <chaselatta@google.com> Commit-Queue: Darren Chan <chandarren@google.com>
diff --git a/googleapis_auth/.gitignore b/googleapis_auth/.gitignore deleted file mode 100644 index 8b2b345..0000000 --- a/googleapis_auth/.gitignore +++ /dev/null
@@ -1,6 +0,0 @@ -.packages -.pub -.dart_tool/ -packages -pubspec.lock -.DS_Store
diff --git a/googleapis_auth/BUILD.gn b/googleapis_auth/BUILD.gn index 774c19c..ea1281a 100644 --- a/googleapis_auth/BUILD.gn +++ b/googleapis_auth/BUILD.gn
@@ -1,34 +1,51 @@ -# This file is generated by importer.py for googleapis_auth-0.2.12+1 +# This file is generated by package_importer.py for googleapis_auth-1.3.0 import("//build/dart/dart_library.gni") dart_library("googleapis_auth") { package_name = "googleapis_auth" - language_version = "2.1" + language_version = "2.13" disable_analysis = true deps = [ "//third_party/dart-pkg/pub/crypto", "//third_party/dart-pkg/pub/http", + "//third_party/dart-pkg/pub/http_parser", ] sources = [ "auth.dart", "auth_browser.dart", "auth_io.dart", + "googleapis_auth.dart", + "src/access_credentials.dart", + "src/access_token.dart", "src/adc_utils.dart", + "src/auth_client.dart", + "src/auth_functions.dart", "src/auth_http_utils.dart", + "src/client_id.dart", "src/crypto/asn1.dart", "src/crypto/pem.dart", "src/crypto/rsa.dart", "src/crypto/rsa_sign.dart", + "src/exceptions.dart", "src/http_client_base.dart", + "src/known_uris.dart", + "src/metadata_server_client.dart", "src/oauth2_flows/auth_code.dart", + "src/oauth2_flows/authorization_code_grant_abstract_flow.dart", + "src/oauth2_flows/authorization_code_grant_manual_flow.dart", + "src/oauth2_flows/authorization_code_grant_server_flow.dart", + "src/oauth2_flows/base_flow.dart", "src/oauth2_flows/implicit.dart", "src/oauth2_flows/jwt.dart", "src/oauth2_flows/metadata_server.dart", + "src/response_type.dart", + "src/service_account_client.dart", + "src/service_account_credentials.dart", "src/typedefs.dart", "src/utils.dart", ]
diff --git a/googleapis_auth/CHANGELOG.md b/googleapis_auth/CHANGELOG.md index 4fa47b2..483bcbf 100644 --- a/googleapis_auth/CHANGELOG.md +++ b/googleapis_auth/CHANGELOG.md
@@ -1,35 +1,86 @@ +## 1.3.0 + +- The `secret` param in `ClientId` constructor is now optional. +- Use the latest supported Google OAuth 2.0 URL +- `auth_browser` library: + - Migrated to newer `auth2` Javascript API. + - Added support for `hostedDomain` to all applicable functions. + - `createImplicitBrowserFlow`: added (unsupported) `enableDebugLogs` param. + (Maybe helpful for debugging, but should not be used in production.) +- `auth_io` library: + - Generate a longer, secure random state token. + - Implement code verifier logic for the desktop auth flows. See + https://developers.google.com/identity/protocols/oauth2/native-app#create-code-challenge + - `obtainAccessCredentialsViaCodeExchange` + - `scopes` are now acquired from the initial API call and not via a separate + API call to the `tokeninfo` endpoint. + - Added optional `codeVerifier` parameter. + +## 1.2.0 + +- Added an optional `hostedDomain` parameter to many functions in + `auth_io.dart`. If provided, restricts sign-in to Google Apps hosted accounts + at that domain. +- Fix an error when doing OAUTH code exchanged with an undefined secret. +- `clientViaApiKey` is now exported from `googleapis_auth.dart`. +- Added `String? details` to `UserConsentException`. +- Update the host used to access metadata on Google Cloud. From + `http://metadata/` to `http://metadata.google.internal`. +- Require Dart 2.13 +- Deprecated `RefreshFailedException` - `ServerRequestFailedException` is used + instead. + +## 1.1.0 + +- Added the `googleapis_auth.dart` library. It is convention to have the default + library within a package align with the package name. `auth.dart` is now + deprecated and will be removed in v2. +- Added `fromJson` factory and `toJson` method to `AccessToken`, + `AccessCredentials`, and `ClientId`. +- Remove dynamic function invocations. + +## 1.0.0 + +- Add support for null-safety. +- Require Dart 2.12 or later. + ## 0.2.12+1 - * Removed a `dart:async` import that isn't required for \>=Dart 2.1. - * Require \>=Dart 2.1. - * Allow null-safe version of `pkg:crypto`. +- Removed a `dart:async` import that isn't required for \>=Dart 2.1. +- Require \>=Dart 2.1. ## 0.2.12 - * Add `clientViaApplicationDefaultCredentials` for obtaining credentials using - [ADC](https://cloud.google.com/docs/authentication/production). + +- Add `clientViaApplicationDefaultCredentials` for obtaining credentials using + [ADC](https://cloud.google.com/docs/authentication/production). ## 0.2.11+1 - * Fix 'multiple completer completion' bug in `ImplicitFlow`. + +- Fix 'multiple completer completion' bug in `ImplicitFlow`. ## 0.2.11 - * Add the `force` parameter to the `obtainAccessCredentialsViaUserConsent` API. + +- Add the `force` parameter to the `obtainAccessCredentialsViaUserConsent` API. ## 0.2.10 - * Look for GCE metadata host in environment under `$GCE_METADATA_HOST`. + +- Look for GCE metadata host in environment under `$GCE_METADATA_HOST`. ## 0.2.9 - * Prepare for [Uint8List SDK breaking change](Prepare for Uint8List SDK breaking change). + +- Prepare for [Uint8List SDK breaking change](Prepare for Uint8List SDK breaking + change). ## 0.2.8 -* Initialize implicit browser flows statically, allowing multiple ImplicitFlow +- Initialize implicit browser flows statically, allowing multiple ImplicitFlow objects to initialize without trying to load the gapi JavaScript library multiple times. ## 0.2.7 - - Support for specifying desired `ResponseType`, allowing applications to - obtain an `id_token` using `ImplicitBrowserFlow`. +- Support for specifying desired `ResponseType`, allowing applications to obtain + an `id_token` using `ImplicitBrowserFlow`. ## 0.2.6 @@ -41,22 +92,22 @@ ## 0.2.5+2 -* Support Dart 2. +- Support Dart 2. ## 0.2.5+1 -* Switch all uppercase constants from `dart:convert` to lowercase. +- Switch all uppercase constants from `dart:convert` to lowercase. ## 0.2.5 -* Add an optional `loginHint` parameter to browser oauth2 flow APIs which can be +- Add an optional `loginHint` parameter to browser oauth2 flow APIs which can be used to specify a hint as to which user is being logged in. ## 0.2.4 -* Added `id_token` to `AccessCredentials` +- Added `id_token` to `AccessCredentials` -* Migrated to Dart 2 `BigInt`. +- Migrated to Dart 2 `BigInt`. ## 0.2.3+6 @@ -82,8 +133,8 @@ ## 0.2.3 -- Allow `ServiceAccountCredentials` constructors to take an optional - `user` argument to specify a user to impersonate. +- Allow `ServiceAccountCredentials` constructors to take an optional `user` + argument to specify a user to impersonate. ## 0.2.2 @@ -91,15 +142,19 @@ - Cleaned up `README.md` ## 0.2.1 + - Added optional `force` and `immediate` arguments to `runHybridFlow`. ## 0.2.0 + - Renamed `forceUserConsent` parameter to `immediate`. - Added `runHybridFlow` function to `auth_browser`, with corresponding `HybridFlowResult` class. ## 0.1.1 + - Add `clientViaApiKey` functions to `auth_io` ad `auth_browser`. ## 0.1.0 + - First release.
diff --git a/googleapis_auth/LICENSE b/googleapis_auth/LICENSE index 5c60afe..000cd7b 100644 --- a/googleapis_auth/LICENSE +++ b/googleapis_auth/LICENSE
@@ -1,4 +1,5 @@ -Copyright 2014, the Dart project authors. All rights reserved. +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: @@ -9,7 +10,7 @@ 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 Inc. nor the names of its + * 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.
diff --git a/googleapis_auth/README.md b/googleapis_auth/README.md index ea03d3f..0b2330c 100644 --- a/googleapis_auth/README.md +++ b/googleapis_auth/README.md
@@ -1,197 +1,204 @@ -## Googleapis Auth - -This package provides support for obtaining OAuth2 credentials to access -Google APIs. +Provides support for obtaining OAuth2 credentials to access Google APIs. This package also provides convenience functionality for: + - obtaining authenticated HTTP clients - automatically refreshing OAuth2 credentials ### Using this package Using this package requires creating a Google Cloud Project and obtaining -application credentials for the specific application type. -The steps required are: +application credentials for the specific application type. The steps required +are: - Create a new Google Cloud Project on the [Google Developers Console](https://console.developers.google.com) - Enable all APIs that the application will use on the - [Google Developers Console](https://console.developers.google.com) - (under DevConsole -> Project -> APIs & auth -> APIs) + [Google Developers Console](https://console.developers.google.com) (under + DevConsole -> Project -> APIs & auth -> APIs) - Obtain application credentials for a specific application type on the - [Google Developers Console](https://console.developers.google.com) - (under DevConsole -> Project -> APIs & auth -> Credentials) -- Use the `googleapis_auth` package to obtain access credentials / - obtain an authenticated HTTP client. + [Google Developers Console](https://console.developers.google.com) (under + DevConsole -> Project -> APIs & auth -> Credentials) +- Use the `googleapis_auth` package to obtain access credentials / obtain an + authenticated HTTP client. -Depending on the application type, there are different ways to achieve the -third and fourth step. The following is a list of supported OAuth2 flows with -a description of these two steps. - +Depending on the application type, there are different ways to achieve the third +and fourth step. The following is a list of supported OAuth2 flows with a +description of these two steps. #### Client-side Web Application -For client-side only web applications a "Client ID" needs to be created -(under DevConsole -> Project -> APIs & auth -> Credentials). When -creating a new client ID, select the "Web application" type. For client-side -only applications, no `Redirect URIs` are necessary. The `Javascript Origins` -setting must be set to all URLs on which your application -will be served (e.g. http://localhost:8080 for local testing). +For client-side only web applications a "Client ID" needs to be created (under +DevConsole -> Project -> APIs & auth -> Credentials). When creating a new client +ID, select the "Web application" type. For client-side only applications, no +`Redirect URIs` are necessary. The `Javascript Origins` setting must be set to +all URLs on which your application will be served (e.g. http://localhost:8080 +for local testing). +After the Client ID has been created, you can obtain access credentials via -After the Client Id has been created, you can obtain access credentials via ```dart -import "package:googleapis_auth/auth_browser.dart"; +import 'package:googleapis_auth/auth_browser.dart'; -... +// Initialize the browser oauth2 flow functionality then use it to obtain credentials. +Future<AccessCredentials> obtainCredentials() async { + final flow = await createImplicitBrowserFlow( + ClientId('....apps.googleusercontent.com'), + ['scope1', 'scope2'], + ); -var id = new ClientId("....apps.googleusercontent.com", null); -var scopes = [...]; - -// Initialize the browser oauth2 flow functionality. -createImplicitBrowserFlow(id, scopes).then((BrowserOAuth2Flow flow) { - flow.obtainAccessCredentialsViaUserConsent() - .then((AccessCredentials credentials) { - // Credentials are available in [credentials]. - ... + try { + return await flow.obtainAccessCredentialsViaUserConsent(); + } finally { flow.close(); - }); -}); + } +} ``` or obtain an authenticated HTTP client via + ```dart -import "package:googleapis_auth/auth_browser.dart"; +import 'package:googleapis_auth/auth_browser.dart'; -... +// Initialize the browser oauth2 flow functionality then use it to +// get an authenticated and auto refreshing client. +Future<AuthClient> obtainAuthenticatedClient() async { + final flow = await createImplicitBrowserFlow( + ClientId('....apps.googleusercontent.com'), + ['scope1', 'scope2'], + ); -var id = new ClientId("....apps.googleusercontent.com", null); -var scopes = [...]; - -// Initialize the browser oauth2 flow functionality. -createImplicitBrowserFlow(id, scopes).then((BrowserOAuth2Flow flow) { - flow.clientViaUserConsent().then((AuthClient client) { - // Authenticated and auto refreshing client is available in [client]. - ... - client.close(); + try { + return await flow.clientViaUserConsent(); + } finally { flow.close(); - }); -}); + } +} ``` To prevent popup blockers from blocking the user authorization dialog, the methods `obtainAccessCredentialsViaUserConsent` and `clientViaUserConsent` -should preferably only be called inside an event handler, since most browsers -do not block popup windows created in response to a user interaction. +should preferably only be called inside an event handler, since most browsers do +not block popup windows created in response to a user interaction. The authenticated HTTP client can now access data on behalf a user for the requested oauth2 scopes. - #### Installed/Console Application -For installed/console applications a "Client ID" needs to be created -(under DevConsole -> Project -> APIs & auth -> Credentials). When -creating a new client ID, select the "Installed application -> Other" type. +For installed/console applications a "Client ID" needs to be created (under +DevConsole -> Project -> APIs & auth -> Credentials). When creating a new client +ID, select the "Installed application -> Other" type. The redirect URIs for the automatic and manual flow will be configured automatically. -After the Client Id has been created, you can obtain access credentials via +After the Client ID has been created, you can obtain access credentials via + ```dart -import "package:http/http.dart" as http; -import "package:googleapis_auth/auth_io.dart"; +import 'package:googleapis_auth/auth_io.dart'; +import 'package:http/http.dart' as http; -... +// Use the oauth2 authentication code flow functionality to obtain +// credentials. [prompt] is used for directing the user to a URI. +Future<AccessCredentials> obtainCredentials() async { + final client = http.Client(); -var id = new ClientId("....apps.googleusercontent.com", "..."); -var scopes = [...]; + try { + return await obtainAccessCredentialsViaUserConsent( + ClientId('....apps.googleusercontent.com', '...'), + ['scope1', 'scope2'], + client, + _prompt, + ); + } finally { + client.close(); + } +} -var client = new http.Client(); -obtainAccessCredentialsViaUserConsent(id, scopes, client, prompt) - .then((AccessCredentials credentials) { - // Access credentials are available in [credentials]. - // ... - client.close(); -}); - -void prompt(String url) { - print("Please go to the following URL and grant access:"); - print(" => $url"); - print(""); +void _prompt(String url) { + print('Please go to the following URL and grant access:'); + print(' => $url'); + print(''); } ``` or obtain an authenticated HTTP client via ```dart -import "package:googleapis_auth/auth_io.dart"; +import 'package:googleapis_auth/auth_io.dart'; -... +// Use the oauth2 code grant server flow functionality to +// get an authenticated and auto refreshing client. +Future<AuthClient> obtainCredentials() async => await clientViaUserConsent( + ClientId('....apps.googleusercontent.com', '...'), + ['scope1', 'scope2'], + _prompt, +); -var id = new ClientId("....apps.googleusercontent.com", "..."); -var scopes = [...]; - -clientViaUserConsent(id, scopes, prompt).then((AuthClient client) { - // Authenticated and auto refreshing client is available in [client]. - // ... - client.close(); -}); - -void prompt(String url) { - print("Please go to the following URL and grant access:"); - print(" => $url"); - print(""); +void _prompt(String url) { + print('Please go to the following URL and grant access:'); + print(' => $url'); + print(''); } ``` +The Client ID must be created with a `client_secret` here, however there is no +way to properly secure a `client_secret` for installed/console applications. +Fortunately the OAuth2 flow used in this case +[assumes that the app cannot keep secrets](https://developers.google.com/identity/protocols/oauth2/native-app) +so this particular `client_secret` does not need to be kept secret. You should +however make sure not to re-use the same `client_secret` anywhere secrecy is +required. + In case of misconfigured browsers/proxies or other issues, it is also possible to use a manual flow via `obtainAccessCredentialsViaUserConsentManual` and `clientViaUserConsentManual`. But in this case the `prompt` function needs to -complete with a `Future<String>` which contains the "authorization code". -The user obtains the "authorization code" (which is a string of characters) in -a browser and needs to copy & paste it to the application. (The prompt function +complete with a `Future<String>` which contains the "authorization code". The +user obtains the "authorization code" (which is a string of characters) in a +browser and needs to copy & paste it to the application. (The prompt function should block until it has gotten the "authorization code" from the user.) The authenticated HTTP client can now access data on behalf a user for the requested oauth2 scopes. - #### Autonomous Application / Service Account If an application wants to act autonomously and access e.g. data from a Google Cloud Project, then a Service Account can be created. In this case no user authorization is involved. -A service account can be created via the "Service account" application type -when creating a new Client ID -(under DevConsole -> Project -> APIs & auth -> Credentials). It will download -a JSON document which contains a private RSA key. That private key is used for -obtaining access credentials. +A service account can be created via the "Service account" application type when +creating a Client ID (under DevConsole -> Project -> APIs & auth -> +Credentials). It will download a JSON document which contains a private RSA key. +That private key is used for obtaining access credentials. After the service account was created, you can obtain access credentials via -```dart -import "package:http/http.dart" as http; -import "package:googleapis_auth/auth_io.dart"; -var accountCredentials = new ServiceAccountCredentials.fromJson({ - "private_key_id": "<please fill in>", - "private_key": "<please fill in>", - "client_email": "<please fill in>@developer.gserviceaccount.com", - "client_id": "<please fill in>.apps.googleusercontent.com", - "type": "service_account" -}); -var scopes = [...]; +```dart +import "package:googleapis_auth/auth_io.dart"; +import "package:http/http.dart" as http; ... -var client = new http.Client(); -obtainAccessCredentialsViaServiceAccount(accountCredentials, scopes, client) - .then((AccessCredentials credentials) { - // Access credentials are available in [credentials]. - // ... +// Use service account credentials to obtain oauth credentials. +Future<AccessCredentials> obtainCredentials() async { + var accountCredentials = ServiceAccountCredentials.fromJson({ + "private_key_id": "<please fill in>", + "private_key": "<please fill in>", + "client_email": "<please fill in>@developer.gserviceaccount.com", + "client_id": "<please fill in>.apps.googleusercontent.com", + "type": "service_account" + }); + var scopes = [...]; + + var client = http.Client(); + AccessCredentials credentials = + await obtainAccessCredentialsViaServiceAccount(accountCredentials, scopes, client); + client.close(); -}); + return credentials; +} ``` or an authenticated HTTP client via @@ -199,38 +206,37 @@ ```dart import "package:googleapis_auth/auth_io.dart"; -final accountCredentials = new ServiceAccountCredentials.fromJson({ - "private_key_id": "<please fill in>", - "private_key": "<please fill in>", - "client_email": "<please fill in>@developer.gserviceaccount.com", - "client_id": "<please fill in>.apps.googleusercontent.com", - "type": "service_account" -}); -var scopes = [...]; - ... -clientViaServiceAccount(accountCredentials, scopes).then((AuthClient client) { - // [client] is an authenticated HTTP client. - // ... - client.close(); -}); +// Use service account credentials to get an authenticated and auto refreshing client. +Future<AuthClient> obtainAuthenticatedClient() async { + final accountCredentials = ServiceAccountCredentials.fromJson({ + "private_key_id": "<please fill in>", + "private_key": "<please fill in>", + "client_email": "<please fill in>@developer.gserviceaccount.com", + "client_id": "<please fill in>.apps.googleusercontent.com", + "type": "service_account" + }); + var scopes = [...]; + + AuthClient client = await clientViaServiceAccount(accountCredentials, scopes); + + return client; // Remember to close the client when you are finished with it. +} ``` The authenticated HTTP client can now access APIs. ##### Impersonation -For some APIs the use of a service account also requires to impersonate a -user. To support that the `ServiceAccountCredentials` constructors have an -optional argument `impersonatedUser` to specify the user to impersonate. +For some APIs the use of a service account also requires to impersonate a user. +To support that the `ServiceAccountCredentials` constructors have an optional +argument `impersonatedUser` to specify the user to impersonate. -One example of this are the Google Apps APIs. See [Perform Google Apps -Domain-Wide Delegation of Authority] -(https://developers.google.com/admin-sdk/directory/v1/guides/delegation) -for information on the additional security configuration required to -enable this for a service account. - +One example of this are the Google Apps APIs. See +[Perform Google Apps Domain-Wide Delegation of Authority](https://developers.google.com/admin-sdk/directory/v1/guides/delegation) +for information on the additional security configuration required to enable this +for a service account. #### Autonomous Application / Compute Engine using metadata service @@ -238,22 +244,28 @@ Cloud Project, then a Service Account can be used. In case the application is running on a ComputeEngine VM it is possible to start a VM with a set of scopes the VM is allowed to use. See the -[documentation](https://developers.google.com/compute/docs/authentication#using) +[documentation](https://cloud.google.com/compute/docs/access/create-enable-service-accounts-for-instances#using) for further information. Here is an example of using the metadata service for obtaining access credentials on a ComputeEngine VM. -```dart -import "package:http/http.dart" as http; -import "package:googleapis_auth/auth_io.dart"; -var client = new http.Client(); -obtainAccessCredentialsViaMetadataServer(client) - .then((AccessCredentials credentials) { - // Access credentials are available in [credentials]. - // ... +```dart +import "package:googleapis_auth/auth_io.dart"; +import "package:http/http.dart" as http; + +... + +// Use the metadata service to obtain oauth credentials. +Future<AccessCredentials> obtainCredentials() async { + var client = http.Client(); + + AccessCredentials credentials = + await obtainAccessCredentialsViaMetadataServer(client); + client.close(); -}); + return credentials; +} ``` or an authenticated HTTP client via @@ -261,27 +273,31 @@ ```dart import "package:googleapis_auth/auth_io.dart"; -clientViaMetadataServer().then((AuthClient client) { - // [client] is an authenticated HTTP client. - // ... - client.close(); -}); -``` -The authenticated HTTP client can now access APIs. +... +// Use the metadata service to get an authenticated and auto refreshing client. +Future<AuthClient> obtainAuthenticatedClient() async { + + AuthClient client = await clientViaMetadataServer(); + + return client; // Remember to close the client when you are finished with it. +} +``` + +The authenticated HTTP client can now access APIs. #### Accessing Public Data with API Key It is possible to access some APIs by just using an API key without OAuth2. -A API key can be obtained on the Google Developers Console by creating a Key -at the "Public API access" section -(under DevConsole -> Project -> APIs & auth -> Credentials). +An API key can be obtained on the Google Developers Console by creating a Key at +the "Public API access" section (under DevConsole -> Project -> APIs & auth -> +Credentials). A key can be created for different application types: For browser applications it is necessary to specify a set of referer URls from which the application -would like to access APIs. For server applications it is possible to specify -a list of IP ranges from which the client application would like to access APIs. +would like to access APIs. For server applications it is possible to specify a +list of IP ranges from which the client application would like to access APIs. Note that the ApiKey is used for quota and billing purposes and should not be disclosed to third parties. @@ -294,12 +310,15 @@ var client = clientViaApiKey('<api-key-from-devconsole>'); // [client] can now be used to make REST calls to Google APIs. -// ... + +... + client.close(); ``` ### More information More information can be obtained from official Google Developers documentation: -- [OAuth2 to Access Google APIs](https://developers.google.com/accounts/docs/OAuth2?hl=fr) + +- [OAuth2 to Access Google APIs](https://developers.google.com/identity/protocols/oauth2) - [OAuth2 Playground](https://developers.google.com/oauthplayground/)
diff --git a/googleapis_auth/analysis_options.yaml b/googleapis_auth/analysis_options.yaml deleted file mode 100644 index 33fd872..0000000 --- a/googleapis_auth/analysis_options.yaml +++ /dev/null
@@ -1,26 +0,0 @@ -linter: - rules: - # Errors - - avoid_empty_else - - control_flow_in_finally - - empty_statements - - test_types_in_equals - - throw_in_finally - - valid_regexps - - # Style - #- annotate_overrides - - avoid_init_to_null - - avoid_return_types_on_setters - - await_only_futures - - camel_case_types - - comment_references - - empty_catches - - empty_constructor_bodies - - hash_and_equals - - library_prefixes - - non_constant_identifier_names - - prefer_is_not_empty - - slash_for_doc_comments - - type_init_formals - - unrelated_type_equality_checks
diff --git a/googleapis_auth/lib/auth.dart b/googleapis_auth/lib/auth.dart index bc12196..d436be2 100644 --- a/googleapis_auth/lib/auth.dart +++ b/googleapis_auth/lib/auth.dart
@@ -1,311 +1,11 @@ -// Copyright (c) 2014, the Dart project authors. Please see the AUTHORS file +// Copyright (c) 2021, 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. -library googleapis_auth.auth; +// ignore_for_file: comment_references -import 'dart:async'; -import 'dart:convert'; +/// This library has been deprecated. Use [googleapis_auth] instead. +@Deprecated('Use `googleapis_auth.dart` instead') +library auth; -import 'package:http/http.dart'; - -import 'src/auth_http_utils.dart'; -import 'src/crypto/pem.dart'; -import 'src/crypto/rsa.dart'; -import 'src/http_client_base.dart'; -import 'src/utils.dart'; - -/// An OAuth2 access token. -class AccessToken { - /// The token type, usually "Bearer" - final String type; - - /// The access token data. - final String data; - - /// Time at which the token will be expired (UTC time) - final DateTime expiry; - - /// [expiry] must be a UTC `DateTime`. - AccessToken(this.type, this.data, this.expiry) { - if (type == null || data == null || expiry == null) { - throw new ArgumentError('Arguments type/data/expiry may not be null.'); - } - - if (!expiry.isUtc) { - throw new ArgumentError('The expiry date must be a Utc DateTime.'); - } - } - - bool get hasExpired { - return new DateTime.now().toUtc().isAfter(expiry); - } - - String toString() => "AccessToken(type=$type, data=$data, expiry=$expiry)"; -} - -/// OAuth2 Credentials. -class AccessCredentials { - /// An access token. - final AccessToken accessToken; - - /// A refresh token, which can be used to refresh the access credentials. - /// - /// This field may be null. - final String refreshToken; - - /// A JWT used in calls to Google APIs that accept an id_token param. - final String idToken; - - /// Scopes these credentials are valid for. - final List<String> scopes; - - AccessCredentials(this.accessToken, this.refreshToken, this.scopes, - {this.idToken}) { - if (accessToken == null || scopes == null) { - throw new ArgumentError('Arguments accessToken/scopes must not be null.'); - } - } -} - -/// Represents the client application's credentials. -class ClientId { - /// The identifier used to identify this application to the server. - final String identifier; - - /// The client secret used to identify this application to the server. - final String secret; - - ClientId(this.identifier, this.secret) { - if (identifier == null) { - throw new ArgumentError('Argument identifier may not be null.'); - } - } - - ClientId.serviceAccount(this.identifier) : secret = null { - if (identifier == null) { - throw new ArgumentError('Argument identifier may not be null.'); - } - } -} - -/// Represents credentials for a service account. -class ServiceAccountCredentials { - /// The email address of this service account. - final String email; - - /// The clientId. - final ClientId clientId; - - /// Private key. - final String privateKey; - - /// Impersonated user, if any. If not impersonating any user this is `null`. - final String impersonatedUser; - - /// Private key as an [RSAPrivateKey]. - final RSAPrivateKey privateRSAKey; - - /// Creates a new [ServiceAccountCredentials] from JSON. - /// - /// [json] can be either a [Map] or a JSON map encoded as a [String]. - /// - /// The optional named argument [impersonatedUser] is used to set the user - /// to impersonate if impersonating a user. - factory ServiceAccountCredentials.fromJson(json, {String impersonatedUser}) { - if (json is String) { - json = jsonDecode(json); - } - if (json is! Map) { - throw new ArgumentError('json must be a Map or a String encoding a Map.'); - } - var identifier = json['client_id']; - var privateKey = json['private_key']; - var email = json['client_email']; - var type = json['type']; - - if (type != 'service_account') { - throw new ArgumentError('The given credentials are not of type ' - 'service_account (was: $type).'); - } - - if (identifier == null || privateKey == null || email == null) { - throw new ArgumentError('The given credentials do not contain all the ' - 'fields: client_id, private_key and client_email.'); - } - - var clientId = new ClientId(identifier, null); - return new ServiceAccountCredentials(email, clientId, privateKey, - impersonatedUser: impersonatedUser); - } - - /// Creates a new [ServiceAccountCredentials]. - /// - /// [email] is the e-mail address of the service account. - /// - /// [clientId] is the client ID for the service account. - /// - /// [privateKey] is the base 64 encoded, unencrypted private key, including - /// the '-----BEGIN PRIVATE KEY-----' and '-----END PRIVATE KEY-----' - /// boundaries. - /// - /// The optional named argument [impersonatedUser] is used to set the user - /// to impersonate if impersonating a user is needed. - ServiceAccountCredentials(this.email, this.clientId, String privateKey, - {this.impersonatedUser}) - : privateKey = privateKey, - privateRSAKey = keyFromString(privateKey) { - if (email == null || clientId == null || privateKey == null) { - throw new ArgumentError( - 'Arguments email/clientId/privateKey must not be null.'); - } - } -} - -/// A authenticated HTTP client. -abstract class AuthClient implements Client { - /// The credentials currently used for making HTTP requests. - AccessCredentials get credentials; -} - -/// A autorefreshing, authenticated HTTP client. -abstract class AutoRefreshingAuthClient implements AuthClient { - /// A broadcast stream of [AccessCredentials]. - /// - /// A listener will get notified when new [AccessCredentials] were obtained. - Stream<AccessCredentials> get credentialUpdates; -} - -/// Thrown if an attempt to refresh a token failed. -class RefreshFailedException implements Exception { - final String message; - RefreshFailedException(this.message); - String toString() => message; -} - -/// Thrown if an attempt to make an authorized request failed. -class AccessDeniedException implements Exception { - final String message; - AccessDeniedException(this.message); - String toString() => message; -} - -/// Thrown if user did not give his consent. -class UserConsentException implements Exception { - final String message; - UserConsentException(this.message); - String toString() => message; -} - -/// Obtain an `http.Client` which automatically authenticates -/// requests using [credentials]. -/// -/// Note that the returned `RequestHandler` will not auto-refresh the given -/// [credentials]. -/// -/// The user is responsible for closing the returned HTTP [Client]. -/// Closing the returned [Client] will not close [baseClient]. -AuthClient authenticatedClient( - Client baseClient, AccessCredentials credentials) { - if (credentials.accessToken.type != 'Bearer') { - throw new ArgumentError('Only Bearer access tokens are accepted.'); - } - return new AuthenticatedClient(baseClient, credentials); -} - -/// Obtain an `http.Client` which automatically refreshes [credentials] -/// before they expire. Uses [baseClient] as a base for making authenticated -/// http requests and for refreshing [credentials]. -/// -/// The user is responsible for closing the returned HTTP [Client]. -/// Closing the returned [Client] will not close [baseClient]. -AutoRefreshingAuthClient autoRefreshingClient( - ClientId clientId, AccessCredentials credentials, Client baseClient) { - if (credentials.accessToken.type != 'Bearer') { - throw new ArgumentError('Only Bearer access tokens are accepted.'); - } - if (credentials.refreshToken == null) { - throw new ArgumentError('Refresh token in AccessCredentials was `null`.'); - } - return new AutoRefreshingClient(baseClient, clientId, credentials); -} - -/// Tries to obtain refreshed [AccessCredentials] based on [credentials] using -/// [client]. -Future<AccessCredentials> refreshCredentials( - ClientId clientId, AccessCredentials credentials, Client client) async { - var formValues = [ - 'client_id=${Uri.encodeComponent(clientId.identifier)}', - 'client_secret=${Uri.encodeComponent(clientId.secret)}', - 'refresh_token=${Uri.encodeComponent(credentials.refreshToken)}', - 'grant_type=refresh_token', - ]; - - var body = new Stream<List<int>>.fromIterable( - [(ascii.encode(formValues.join('&')))]); - var request = new RequestImpl('POST', _googleTokenUri, body); - request.headers['content-type'] = 'application/x-www-form-urlencoded'; - - var response = await client.send(request); - var contentType = response.headers['content-type']; - contentType = contentType == null ? null : contentType.toLowerCase(); - - if (contentType == null || - (!contentType.contains('json') && !contentType.contains('javascript'))) { - await response.stream.drain().catchError((_) {}); - throw new Exception( - 'Server responded with invalid content type: $contentType. ' - 'Expected json response.'); - } - - var object = await response.stream - .transform(ascii.decoder) - .transform(json.decoder) - .first; - - var jsonMap = object as Map; - - var idToken = jsonMap['id_token']; - var token = jsonMap['access_token']; - var seconds = jsonMap['expires_in']; - var tokenType = jsonMap['token_type']; - var error = jsonMap['error']; - - if (response.statusCode != 200 && error != null) { - throw new RefreshFailedException('Refreshing attempt failed. ' - 'Response was ${response.statusCode}. Error message was $error.'); - } - - if (token == null || seconds is! int || tokenType != 'Bearer') { - throw new Exception('Refresing attempt failed. ' - 'Invalid server response.'); - } - - return new AccessCredentials( - new AccessToken(tokenType, token, expiryDate(seconds)), - credentials.refreshToken, - credentials.scopes, - idToken: idToken); -} - -/// Available response types that can be requested when using the implicit -/// browser login flow. -/// -/// More information about these values can be found here: -/// https://developers.google.com/identity/protocols/OpenIDConnect#response-type -enum ResponseType { - /// Requests an access code. This triggers the basic rather than the implicit - /// flow. - code, - - /// Requests the user's identity token when running the implicit flow. - idToken, - - /// Requests the user's current permissions. - permission, - - /// Requests the user's access token when running the implicit flow. - token, -} - -final _googleTokenUri = Uri.parse('https://accounts.google.com/o/oauth2/token'); +export 'googleapis_auth.dart';
diff --git a/googleapis_auth/lib/auth_browser.dart b/googleapis_auth/lib/auth_browser.dart index 5f6cc4e..67800c6 100644 --- a/googleapis_auth/lib/auth_browser.dart +++ b/googleapis_auth/lib/auth_browser.dart
@@ -2,36 +2,18 @@ // 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. -library googleapis_auth.auth_browser; - import 'dart:async'; import 'package:http/browser_client.dart'; import 'package:http/http.dart'; -import 'auth.dart'; +import 'src/auth_functions.dart'; import 'src/auth_http_utils.dart'; import 'src/http_client_base.dart'; import 'src/oauth2_flows/implicit.dart'; +import 'src/service_account_credentials.dart'; -export 'auth.dart'; - -/// Obtains a HTTP client which uses the given [apiKey] for making HTTP -/// requests. -/// -/// Note that the returned client should *only* be used for making HTTP requests -/// to Google Services. The [apiKey] should not be disclosed to third parties. -/// -/// The user is responsible for closing the returned HTTP [Client]. -/// Closing the returned [Client] will not close [baseClient]. -Client clientViaApiKey(String apiKey, {Client baseClient}) { - if (baseClient == null) { - baseClient = new BrowserClient(); - } else { - baseClient = nonClosingClient(baseClient); - } - return new ApiKeyClient(baseClient, apiKey); -} +export 'googleapis_auth.dart'; /// Will create and complete with a [BrowserOAuth2Flow] object. /// @@ -44,36 +26,46 @@ /// If loading or initializing the `gapi` library results in an error, this /// future will complete with an error. /// -/// If [baseClient] is not given, one will be automatically created. It will be -/// used for making authenticated HTTP requests. See [BrowserOAuth2Flow]. +/// {@macro googleapis_auth_clientId_param} /// -/// The [ClientId] can be obtained in the Google Cloud Console. +/// {@template googleapis_auth_baseClient_param} +/// If [baseClient] is provided, all HTTP requests will be made with it. +/// Otherwise, a new [Client] instance will be created. +/// {@endtemplate} /// -/// The user is responsible for closing the returned [BrowserOAuth2Flow] object. -/// Closing the returned [BrowserOAuth2Flow] will not close [baseClient] -/// if one was given. +/// {@macro googleapis_auth_close_the_client} +/// {@macro googleapis_auth_not_close_the_baseClient} Future<BrowserOAuth2Flow> createImplicitBrowserFlow( - ClientId clientId, List<String> scopes, - {Client baseClient}) { - if (baseClient == null) { - baseClient = new RefCountedClient(new BrowserClient(), initialRefCount: 1); - } else { - baseClient = new RefCountedClient(baseClient, initialRefCount: 2); - } + ClientId clientId, + List<String> scopes, { + Client? baseClient, + @Deprecated( + 'Undocumented feature. May help debugging. ' + 'Do not include in production code.', + ) + bool enableDebugLogs = false, +}) async { + final refCountedClient = baseClient == null + ? RefCountedClient(BrowserClient()) + : RefCountedClient(baseClient, initialRefCount: 2); - var flow = new ImplicitFlow(clientId.identifier, scopes); - return flow.initialize().catchError((error, stack) { - baseClient.close(); - return new Future.error(error, stack); - }).then((_) => new BrowserOAuth2Flow._(flow, baseClient)); + final flow = ImplicitFlow(clientId.identifier, scopes, enableDebugLogs); + + try { + await flow.initialize(); + } catch (_) { + refCountedClient.close(); + rethrow; + } + return BrowserOAuth2Flow._(flow, refCountedClient); } /// Used for obtaining oauth2 access credentials. /// /// Warning: /// -/// The methods `obtainAccessCredentialsViaUserConsent` and -/// `clientViaUserConsent` try to open a popup window for the user authorization +/// The methods [obtainAccessCredentialsViaUserConsent] and +/// [clientViaUserConsent] try to open a popup window for the user authorization /// dialog. /// /// In order to prevent browsers from blocking the popup window, these @@ -92,55 +84,68 @@ /// Obtain oauth2 [AccessCredentials]. /// + /// {@template googleapis_auth_force} + /// If [force] is `true` this will create a popup window and ask the user to + /// grant the application offline access. In case the user is not already + /// logged in, they will be presented with an login dialog first. + /// + /// If [force] is `false` this will only create a popup window if the user + /// has not already granted the application access. + /// {@endtemplate} + /// + /// {@template googleapis_auth_immediate} /// If [immediate] is `true` there will be no user involvement. If the user /// is either not logged in or has not already granted the application access, /// a `UserConsentException` will be thrown. /// - /// If [immediate] is `false` the user might be asked to login (if he is not + /// If [immediate] is `false` the user might be asked to login (if not /// already logged in) and might get asked to grant the application access /// (if the application hasn't been granted access before). + /// {@endtemplate} /// - /// If [force] is `true` this will create a popup window and ask the user to - /// grant the application offline access. In case the user is not already - /// logged in, he will be presented with an login dialog first. - /// - /// If [force] is `false` this will only create a popup window if the user - /// has not already granted the application access. - /// + /// {@template googleapis_auth_loginHint} /// If [loginHint] is not `null`, it will be passed to the server as a hint /// to which user is being signed-in. This can e.g. be an email or a User ID /// which might be used as pre-selection in the sign-in flow. + /// {@endtemplate} + /// + /// {@macro googleapis_auth_hostedDomain_param} /// /// If [responseTypes] is not `null` or empty, it will be sent to the server /// to inform the server of the type of responses to reply with. /// - /// The returned future will complete with `AccessCredentials` if the user - /// has given the application access to it's data. Otherwise the future will - /// complete with a `UserConsentException`. + /// {@template googleapis_auth_user_consent_return} + /// The returned [Future] will complete with [AccessCredentials] if the user + /// has given the application access to their data. + /// Otherwise, a [UserConsentException] will be thrown. + /// {@endtemplate} /// - /// In case another error occurs the returned future will complete with an - /// `Exception`. - Future<AccessCredentials> obtainAccessCredentialsViaUserConsent( - {bool immediate: false, - bool force: false, - String loginHint, - List<ResponseType> responseTypes}) { + /// {@macro googleapis_auth_hostedDomain_param} + Future<AccessCredentials> obtainAccessCredentialsViaUserConsent({ + bool force = false, + bool immediate = false, + String? loginHint, + List<ResponseType>? responseTypes, + String? hostedDomain, + }) { _ensureOpen(); return _flow.login( - force: force, - immediate: immediate, - loginHint: loginHint, - responseTypes: responseTypes); + prompt: _promptFromBooleans(force, immediate), + loginHint: loginHint, + responseTypes: responseTypes, + hostedDomain: hostedDomain, + ); } /// Obtains [AccessCredentials] and returns an authenticated HTTP client. /// - /// After obtaining access credentials, this function will return an HTTP - /// [Client]. HTTP requests made on the returned client will get an - /// additional `Authorization` header with the `AccessCredentials` obtained. - /// - /// In case the `AccessCredentials` expire, it will try to obtain new ones - /// without user consent. + /// {@template googleapis_auth_returned_auto_refresh_client} + /// HTTP requests made on the returned client will get an additional + /// `Authorization` header with the [AccessCredentials] obtained. + /// Once the [AccessCredentials] expire, it will use it's refresh token + /// (if available) to obtain new credentials. + /// See [autoRefreshingClient] for more information. + /// {@endtemplate} /// /// See [obtainAccessCredentialsViaUserConsent] for how credentials will be /// obtained. Errors from [obtainAccessCredentialsViaUserConsent] will be let @@ -150,12 +155,24 @@ /// The returned HTTP client will forward errors from lower levels via it's /// `Future<Response>` or it's `Response.read()` stream. /// - /// The user is responsible for closing the returned HTTP client. - Future<AutoRefreshingAuthClient> clientViaUserConsent( - {bool immediate: false, String loginHint}) { - return obtainAccessCredentialsViaUserConsent( - immediate: immediate, loginHint: loginHint) - .then(_clientFromCredentials); + /// {@macro googleapis_auth_immediate} + /// + /// {@macro googleapis_auth_close_the_client} + /// + /// {@macro googleapis_auth_loginHint} + /// + /// {@macro googleapis_auth_hostedDomain_param} + Future<AutoRefreshingAuthClient> clientViaUserConsent({ + bool immediate = false, + String? loginHint, + String? hostedDomain, + }) async { + final credentials = await obtainAccessCredentialsViaUserConsent( + immediate: immediate, + loginHint: loginHint, + hostedDomain: hostedDomain, + ); + return _clientFromCredentials(credentials); } /// Obtains [AccessCredentials] and an authorization code which can be @@ -168,29 +185,26 @@ /// credentials to make API calls, but the server may want to have offline /// access to user data as well. /// - /// If [force] is `true` this will create a popup window and ask the user to - /// grant the application offline access. In case the user is not already - /// logged in, he will be presented with an login dialog first. + /// {@macro googleapis_auth_force} /// - /// If [force] is `false` this will only create a popup window if the user - /// has not already granted the application access. Please note that the - /// authorization code can only be exchanged for a refresh token if the user - /// had to grant access via the popup window. Otherwise the code exchange - /// will only give an access token. + /// {@macro googleapis_auth_immediate} /// - /// If [loginHint] is not `null`, it will be passed to the server as a hint - /// to which user is being signed-in. This can e.g. be an email or a User ID - /// which might be used as pre-selection in the sign-in flow. + /// {@macro googleapis_auth_loginHint} /// - /// If [immediate] is `true` there will be no user involvement. If the user - /// is either not logged in or has not already granted the application access, - /// a `UserConsentException` will be thrown. - Future<HybridFlowResult> runHybridFlow( - {bool force: true, bool immediate: false, String loginHint}) async { + /// {@macro googleapis_auth_hostedDomain_param} + Future<HybridFlowResult> runHybridFlow({ + bool force = true, + bool immediate = false, + String? loginHint, + String? hostedDomain, + }) async { _ensureOpen(); - var result = await _flow.loginHybrid( - force: force, immediate: immediate, loginHint: loginHint); - return new HybridFlowResult(this, result.credential, result.code); + final result = await _flow.loginHybrid( + prompt: _promptFromBooleans(force, immediate), + hostedDomain: hostedDomain, + loginHint: loginHint, + ); + return HybridFlowResult(this, result.credential, result.code); } /// Will close this [BrowserOAuth2Flow] object and the HTTP [Client] it is @@ -214,14 +228,14 @@ void _ensureOpen() { if (_wasClosed) { - throw new StateError('BrowserOAuth2Flow has already been closed.'); + throw StateError('BrowserOAuth2Flow has already been closed.'); } } AutoRefreshingAuthClient _clientFromCredentials(AccessCredentials cred) { _ensureOpen(); _client.acquire(); - return new _AutoRefreshingBrowserClient(_client, cred, _flow); + return _AutoRefreshingBrowserClient(_client, cred, _flow); } } @@ -247,7 +261,7 @@ /// /// The auth code can be used to receive permanent access credentials. /// This requires a confidential client which can keep a secret. - final String authorizationCode; + final String? authorizationCode; HybridFlowResult(this._flow, this.credentials, this.authorizationCode); @@ -258,25 +272,40 @@ } class _AutoRefreshingBrowserClient extends AutoRefreshDelegatingClient { + @override AccessCredentials credentials; - ImplicitFlow _flow; + final ImplicitFlow _flow; Client _authClient; _AutoRefreshingBrowserClient(Client client, this.credentials, this._flow) - : super(client) { - _authClient = authenticatedClient(baseClient, credentials); - } + : _authClient = authenticatedClient(client, credentials), + super(client); - Future<StreamedResponse> send(BaseRequest request) { + @override + Future<StreamedResponse> send(BaseRequest request) async { if (!credentials.accessToken.hasExpired) { return _authClient.send(request); - } else { - return _flow.login(immediate: true).then((newCredentials) { - credentials = newCredentials; - notifyAboutNewCredentials(credentials); - _authClient = authenticatedClient(baseClient, credentials); - return _authClient.send(request); - }); } + credentials = await _flow.login(prompt: 'none'); + notifyAboutNewCredentials(credentials); + _authClient = authenticatedClient(baseClient, credentials); + return _authClient.send(request); } } + +String? _promptFromBooleans(bool force, bool immediate) { + if (force) { + if (immediate) { + throw ArgumentError.value( + immediate, + 'immediate', + 'Cannot be true if `force` is also true.', + ); + } + return 'consent'; + } + if (immediate) { + return 'none'; + } + return null; +}
diff --git a/googleapis_auth/lib/auth_io.dart b/googleapis_auth/lib/auth_io.dart index 4a311ed..63daf15 100644 --- a/googleapis_auth/lib/auth_io.dart +++ b/googleapis_auth/lib/auth_io.dart
@@ -2,30 +2,34 @@ // 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. -library googleapis_auth.auth_io; - import 'dart:io'; import 'package:http/http.dart'; -import 'auth.dart'; -import 'src/auth_http_utils.dart'; import 'src/adc_utils.dart'; +import 'src/auth_http_utils.dart'; import 'src/http_client_base.dart'; -import 'src/oauth2_flows/auth_code.dart'; -import 'src/oauth2_flows/jwt.dart'; -import 'src/oauth2_flows/metadata_server.dart'; +import 'src/metadata_server_client.dart' show clientViaMetadataServer; +import 'src/oauth2_flows/authorization_code_grant_manual_flow.dart'; +import 'src/oauth2_flows/authorization_code_grant_server_flow.dart'; +import 'src/service_account_credentials.dart'; import 'src/typedefs.dart'; -export 'auth.dart'; +export 'googleapis_auth.dart'; +export 'src/metadata_server_client.dart'; +export 'src/oauth2_flows/auth_code.dart' + show obtainAccessCredentialsViaCodeExchange; +export 'src/service_account_client.dart'; export 'src/typedefs.dart'; -/// Create a client using [Application Default Credentials][ADC]. +/// Create a client using +/// [Application Default Credentials](https://cloud.google.com/docs/authentication/production). /// /// Looks for credentials in the following order of preference: /// 1. A JSON file whose path is specified by `GOOGLE_APPLICATION_CREDENTIALS`, /// this file typically contains [exported service account keys][svc-keys]. -/// 2. A JSON file created by [`gcloud auth application-default login`][gcloud-login] +/// 2. A JSON file created by +/// [`gcloud auth application-default login`][gcloud-login] /// in a well-known location (`%APPDATA%/gcloud/application_default_credentials.json` /// on Windows and `$HOME/.config/gcloud/application_default_credentials.json` on Linux/Mac). /// 3. On Google Compute Engine and App Engine Flex we fetch credentials from @@ -34,13 +38,16 @@ /// [metadata]: https://cloud.google.com/compute/docs/storing-retrieving-metadata /// [svc-keys]: https://cloud.google.com/docs/authentication/getting-started /// [gcloud-login]: https://cloud.google.com/sdk/gcloud/reference/auth/application-default/login -/// [ADC]: https://cloud.google.com/docs/authentication/production +/// +/// {@macro googleapis_auth_baseClient_param} +/// +/// {@macro googleapis_auth_returned_auto_refresh_client} Future<AutoRefreshingAuthClient> clientViaApplicationDefaultCredentials({ - List<String> scopes, - Client baseClient, + required List<String> scopes, + Client? baseClient, }) async { if (baseClient == null) { - baseClient = new Client(); + baseClient = Client(); } else { baseClient = nonClosingClient(baseClient); } @@ -61,11 +68,15 @@ // Attempt to use file created by `gcloud auth application-default login` File credFile; if (Platform.isWindows) { - credFile = File.fromUri(Uri.directory(Platform.environment['APPDATA']) - .resolve('gcloud/application_default_credentials.json')); + credFile = File.fromUri( + Uri.directory(Platform.environment['APPDATA']!) + .resolve('gcloud/application_default_credentials.json'), + ); } else { - credFile = File.fromUri(Uri.directory(Platform.environment['HOME']) - .resolve('.config/gcloud/application_default_credentials.json')); + credFile = File.fromUri( + Uri.directory(Platform.environment['HOME']!) + .resolve('.config/gcloud/application_default_credentials.json'), + ); } // Only try to load from credFile if it exists. if (await credFile.exists()) { @@ -85,27 +96,40 @@ /// See [obtainAccessCredentialsViaUserConsent] for specifics about the /// arguments used for obtaining access credentials. /// -/// Once access credentials have been obtained, this function will complete -/// with an auto-refreshing HTTP client. Once the `AccessCredentials` expire -/// it will use it's refresh token (if available) to obtain new credentials. -/// See [autoRefreshingClient] for more information. +/// {@macro googleapis_auth_clientId_param} /// -/// If [baseClient] is not given, one will be automatically created. It will be -/// used for making authenticated HTTP requests. +/// {@macro googleapis_auth_returned_auto_refresh_client} /// -/// The user is responsible for closing the returned HTTP [Client]. -/// Closing the returned [Client] will not close [baseClient]. +/// {@macro googleapis_auth_baseClient_param} +/// +/// {@template googleapis_auth_hostedDomain_param} +/// If provided, restricts sign-in to Google Apps hosted accounts at +/// [hostedDomain]. For more details, see +/// https://developers.google.com/identity/protocols/oauth2/openid-connect#hd-param +/// {@endtemplate} +/// +/// {@macro googleapis_auth_close_the_client} +/// {@macro googleapis_auth_not_close_the_baseClient} Future<AutoRefreshingAuthClient> clientViaUserConsent( - ClientId clientId, List<String> scopes, PromptUserForConsent userPrompt, - {Client baseClient}) async { - bool closeUnderlyingClient = false; + ClientId clientId, + List<String> scopes, + PromptUserForConsent userPrompt, { + Client? baseClient, + String? hostedDomain, +}) async { + var closeUnderlyingClient = false; if (baseClient == null) { - baseClient = new Client(); + baseClient = Client(); closeUnderlyingClient = true; } - var flow = new AuthorizationCodeGrantServerFlow( - clientId, scopes, baseClient, userPrompt); + final flow = AuthorizationCodeGrantServerFlow( + clientId, + scopes, + baseClient, + userPrompt, + hostedDomain: hostedDomain, + ); AccessCredentials credentials; @@ -117,8 +141,12 @@ } rethrow; } - return new AutoRefreshingClient(baseClient, clientId, credentials, - closeUnderlyingClient: closeUnderlyingClient); + return AutoRefreshingClient( + baseClient, + clientId, + credentials, + closeUnderlyingClient: closeUnderlyingClient, + ); } /// Obtains oauth2 credentials and returns an authenticated HTTP client. @@ -126,27 +154,36 @@ /// See [obtainAccessCredentialsViaUserConsentManual] for specifics about the /// arguments used for obtaining access credentials. /// -/// Once access credentials have been obtained, this function will complete -/// with an auto-refreshing HTTP client. Once the `AccessCredentials` expire -/// it will use it's refresh token (if available) to obtain new credentials. -/// See [autoRefreshingClient] for more information. +/// {@macro googleapis_auth_clientId_param} /// -/// If [baseClient] is not given, one will be automatically created. It will be -/// used for making authenticated HTTP requests. +/// {@macro googleapis_auth_returned_auto_refresh_client} /// -/// The user is responsible for closing the returned HTTP [Client]. -/// Closing the returned [Client] will not close [baseClient]. -Future<AutoRefreshingAuthClient> clientViaUserConsentManual(ClientId clientId, - List<String> scopes, PromptUserForConsentManual userPrompt, - {Client baseClient}) async { - bool closeUnderlyingClient = false; +/// {@macro googleapis_auth_baseClient_param} +/// +/// {@macro googleapis_auth_hostedDomain_param} +/// +/// {@macro googleapis_auth_close_the_client} +/// {@macro googleapis_auth_not_close_the_baseClient} +Future<AutoRefreshingAuthClient> clientViaUserConsentManual( + ClientId clientId, + List<String> scopes, + PromptUserForConsentManual userPrompt, { + Client? baseClient, + String? hostedDomain, +}) async { + var closeUnderlyingClient = false; if (baseClient == null) { - baseClient = new Client(); + baseClient = Client(); closeUnderlyingClient = true; } - var flow = new AuthorizationCodeGrantManualFlow( - clientId, scopes, baseClient, userPrompt); + final flow = AuthorizationCodeGrantManualFlow( + clientId, + scopes, + baseClient, + userPrompt, + hostedDomain: hostedDomain, + ); AccessCredentials credentials; @@ -159,261 +196,64 @@ rethrow; } - return new AutoRefreshingClient(baseClient, clientId, credentials, - closeUnderlyingClient: closeUnderlyingClient); -} - -/// Obtains oauth2 credentials and returns an authenticated HTTP client. -/// -/// See [obtainAccessCredentialsViaServiceAccount] for specifics about the -/// arguments used for obtaining access credentials. -/// -/// Once access credentials have been obtained, this function will complete -/// with an auto-refreshing HTTP client. Once the `AccessCredentials` expire -/// it will obtain new access credentials. -/// -/// If [baseClient] is not given, one will be automatically created. It will be -/// used for making authenticated HTTP requests and for obtaining access -/// credentials. -/// -/// The user is responsible for closing the returned HTTP [Client]. -/// Closing the returned [Client] will not close [baseClient]. -Future<AutoRefreshingAuthClient> clientViaServiceAccount( - ServiceAccountCredentials clientCredentials, List<String> scopes, - {Client baseClient}) async { - if (baseClient == null) { - baseClient = new Client(); - } else { - baseClient = nonClosingClient(baseClient); - } - - var flow = new JwtFlow( - clientCredentials.email, - clientCredentials.privateRSAKey, - clientCredentials.impersonatedUser, - scopes, - baseClient); - - AccessCredentials credentials; - try { - credentials = await flow.run(); - } catch (e) { - baseClient.close(); - rethrow; - } - - return new _ServiceAccountClient(baseClient, credentials, flow); -} - -/// Obtains oauth2 credentials and returns an authenticated HTTP client. -/// -/// See [obtainAccessCredentialsViaMetadataServer] for specifics about the -/// arguments used for obtaining access credentials. -/// -/// Once access credentials have been obtained, this function will complete -/// with an auto-refreshing HTTP client. Once the `AccessCredentials` expire -/// it will obtain new access credentials. -/// -/// If [baseClient] is not given, one will be automatically created. It will be -/// used for making authenticated HTTP requests and for obtaining access -/// credentials. -/// -/// The user is responsible for closing the returned HTTP [Client]. -/// Closing the returned [Client] will not close [baseClient]. -Future<AutoRefreshingAuthClient> clientViaMetadataServer( - {Client baseClient}) async { - if (baseClient == null) { - baseClient = new Client(); - } else { - baseClient = nonClosingClient(baseClient); - } - - var flow = new MetadataServerAuthorizationFlow(baseClient); - - AccessCredentials credentials; - - try { - credentials = await flow.run(); - } catch (e) { - baseClient.close(); - rethrow; - } - return new _MetadataServerClient(baseClient, credentials, flow); -} - -/// Obtains a HTTP client which uses the given [apiKey] for making HTTP -/// requests. -/// -/// Note that the returned client should *only* be used for making HTTP requests -/// to Google Services. The [apiKey] should not be disclosed to third parties. -/// -/// The user is responsible for closing the returned HTTP [Client]. -/// Closing the returned [Client] will not close [baseClient]. -Client clientViaApiKey(String apiKey, {Client baseClient}) { - if (baseClient == null) { - baseClient = new Client(); - } else { - baseClient = nonClosingClient(baseClient); - } - return new ApiKeyClient(baseClient, apiKey); + return AutoRefreshingClient( + baseClient, + clientId, + credentials, + closeUnderlyingClient: closeUnderlyingClient, + ); } /// Obtain oauth2 [AccessCredentials] using the oauth2 authentication code flow. /// -/// The returned future will complete with `AccessCredentials` if the user -/// has given the application access to it's data. Otherwise the future will -/// complete with a `UserConsentException`. -/// -/// In case another error occurs the returned future will complete with an -/// `Exception`. +/// {@macro googleapis_auth_clientId_param} /// /// [userPrompt] will be used for directing the user/user-agent to a URI. See /// [PromptUserForConsent] for more information. /// -/// [client] will be used for obtaining `AccessCredentials` from an -/// authorization code. +/// {@macro googleapis_auth_client_for_creds} /// -/// The [ClientId] can be obtained in the Google Cloud Console. +/// {@macro googleapis_auth_hostedDomain_param} +/// +/// {@macro googleapis_auth_user_consent_return} Future<AccessCredentials> obtainAccessCredentialsViaUserConsent( - ClientId clientId, - List<String> scopes, - Client client, - PromptUserForConsent userPrompt) { - return new AuthorizationCodeGrantServerFlow( - clientId, scopes, client, userPrompt) - .run(); -} + ClientId clientId, + List<String> scopes, + Client client, + PromptUserForConsent userPrompt, { + String? hostedDomain, +}) => + AuthorizationCodeGrantServerFlow( + clientId, + scopes, + client, + userPrompt, + hostedDomain: hostedDomain, + ).run(); /// Obtain oauth2 [AccessCredentials] using the oauth2 authentication code flow. /// -/// The returned future will complete with `AccessCredentials` if the user -/// has given the application access to it's data. Otherwise the future will -/// complete with a `UserConsentException`. -/// -/// In case another error occurs the returned future will complete with an -/// `Exception`. +/// {@macro googleapis_auth_clientId_param} /// /// [userPrompt] will be used for directing the user/user-agent to a URI. See /// [PromptUserForConsentManual] for more information. /// -/// [client] will be used for obtaining `AccessCredentials` from an -/// authorization code. +/// {@macro googleapis_auth_client_for_creds} /// -/// The [ClientId] can be obtained in the Google Cloud Console. +/// {@macro googleapis_auth_hostedDomain_param} +/// +/// {@macro googleapis_auth_user_consent_return} Future<AccessCredentials> obtainAccessCredentialsViaUserConsentManual( - ClientId clientId, - List<String> scopes, - Client client, - PromptUserForConsentManual userPrompt) { - return new AuthorizationCodeGrantManualFlow( - clientId, scopes, client, userPrompt) - .run(); -} - -/// Obtain oauth2 [AccessCredentials] using service account credentials. -/// -/// In case the service account has no access to the requested scopes or another -/// error occurs the returned future will complete with an `Exception`. -/// -/// [baseClient] will be used for obtaining `AccessCredentials`. -/// -/// The [ServiceAccountCredentials] can be obtained in the Google Cloud Console. -Future<AccessCredentials> obtainAccessCredentialsViaServiceAccount( - ServiceAccountCredentials clientCredentials, - List<String> scopes, - Client baseClient) { - return new JwtFlow(clientCredentials.email, clientCredentials.privateRSAKey, - clientCredentials.impersonatedUser, scopes, baseClient) - .run(); -} - -/// Obtain oauth2 [AccessCredentials] using the metadata API on ComputeEngine. -/// -/// In case the VM was not configured with access to the requested scopes or an -/// error occurs the returned future will complete with an `Exception`. -/// -/// [baseClient] will be used for obtaining `AccessCredentials`. -/// -/// No credentials are needed. But this function is only intended to work on a -/// Google Compute Engine VM with configured access to Google APIs. -Future<AccessCredentials> obtainAccessCredentialsViaMetadataServer( - Client baseClient) { - return new MetadataServerAuthorizationFlow(baseClient).run(); -} - -/// Obtain oauth2 [AccessCredentials] by exchanging an authorization code. -/// -/// Running a hybrid oauth2 flow as described in the -/// `googleapis_auth.auth_browser` library results in a `HybridFlowResult` which -/// contains short-lived [AccessCredentials] for the client and an authorization -/// code. This authorization code needs to be transferred to the server, which -/// can exchange it against long-lived [AccessCredentials]. -/// -/// If the authorization code was obtained using the mentioned hybrid flow, the -/// [redirectUrl] must be `"postmessage"` (default). -/// -/// If you obtained the authorization code using a different mechanism, the -/// [redirectUrl] must be the same that was used to obtain the code. -/// -/// NOTE: Only the server application will know the `client secret` - which is -/// necessary to exchange an authorization code against access tokens. -/// -/// NOTE: It is important to transmit the authorization code in a secure manner -/// to the server. You should use "anti-request forgery state tokens" to guard -/// against "cross site request forgery" attacks. -/// An example on how to do this can be found here: -/// https://developers.google.com/+/web/signin/server-side-flow -Future<AccessCredentials> obtainAccessCredentialsViaCodeExchange( - Client baseClient, ClientId clientId, String code, - {String redirectUrl: 'postmessage'}) { - return obtainAccessCredentialsUsingCode( - clientId, code, redirectUrl, baseClient); -} - -/// Will close the underlying `http.Client`. -class _ServiceAccountClient extends AutoRefreshDelegatingClient { - final JwtFlow flow; - AccessCredentials credentials; - Client authClient; - - _ServiceAccountClient(Client client, this.credentials, this.flow) - : super(client) { - authClient = authenticatedClient(baseClient, credentials); - } - - Future<StreamedResponse> send(BaseRequest request) async { - if (!credentials.accessToken.hasExpired) { - return authClient.send(request); - } else { - var newCredentials = await flow.run(); - notifyAboutNewCredentials(newCredentials); - credentials = newCredentials; - authClient = authenticatedClient(baseClient, credentials); - return authClient.send(request); - } - } -} - -/// Will close the underlying `http.Client`. -class _MetadataServerClient extends AutoRefreshDelegatingClient { - final MetadataServerAuthorizationFlow flow; - AccessCredentials credentials; - Client authClient; - - _MetadataServerClient(Client client, this.credentials, this.flow) - : super(client) { - authClient = authenticatedClient(baseClient, credentials); - } - - Future<StreamedResponse> send(BaseRequest request) async { - if (!credentials.accessToken.hasExpired) { - return authClient.send(request); - } - - var newCredentials = await flow.run(); - notifyAboutNewCredentials(newCredentials); - credentials = newCredentials; - authClient = authenticatedClient(baseClient, credentials); - return authClient.send(request); - } -} + ClientId clientId, + List<String> scopes, + Client client, + PromptUserForConsentManual userPrompt, { + String? hostedDomain, +}) => + AuthorizationCodeGrantManualFlow( + clientId, + scopes, + client, + userPrompt, + hostedDomain: hostedDomain, + ).run();
diff --git a/googleapis_auth/lib/googleapis_auth.dart b/googleapis_auth/lib/googleapis_auth.dart new file mode 100644 index 0000000..ce2033a --- /dev/null +++ b/googleapis_auth/lib/googleapis_auth.dart
@@ -0,0 +1,33 @@ +// Copyright (c) 2021, 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. + +// ignore_for_file: comment_references + +/// Contains common libraries used across the package. +/// +/// In most cases, you'll want to import either +/// [auth_io] or [auth_browser] depending on your platform. +/// {@canonicalFor access_credentials.AccessCredentials} +/// {@canonicalFor access_token.AccessToken} +/// {@canonicalFor auth_client.AuthClient} +/// {@canonicalFor auth_client.AutoRefreshingAuthClient} +/// {@canonicalFor auth_functions.authenticatedClient} +/// {@canonicalFor auth_functions.autoRefreshingClient} +/// {@canonicalFor auth_functions.clientViaApiKey} +/// {@canonicalFor auth_functions.refreshCredentials} +/// {@canonicalFor client_id.ClientId} +/// {@canonicalFor exceptions.AccessDeniedException} +/// {@canonicalFor exceptions.ServerRequestFailedException} +/// {@canonicalFor exceptions.RefreshFailedException} +/// {@canonicalFor exceptions.UserConsentException} +/// {@canonicalFor response_type.ResponseType} +/// {@canonicalFor service_account_credentials.ServiceAccountCredentials} +library googleapis_auth; + +export 'src/auth_client.dart'; +export 'src/auth_functions.dart'; +export 'src/client_id.dart'; +export 'src/exceptions.dart'; +export 'src/response_type.dart'; +export 'src/service_account_credentials.dart';
diff --git a/googleapis_auth/lib/src/access_credentials.dart b/googleapis_auth/lib/src/access_credentials.dart new file mode 100644 index 0000000..1c7d8a5 --- /dev/null +++ b/googleapis_auth/lib/src/access_credentials.dart
@@ -0,0 +1,42 @@ +// Copyright (c) 2021, 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 'access_token.dart'; + +/// OAuth2 Credentials. +class AccessCredentials { + /// An access token. + final AccessToken accessToken; + + /// A refresh token, which can be used to refresh the access credentials. + final String? refreshToken; + + /// A JWT used in calls to Google APIs that accept an id_token param. + final String? idToken; + + /// Scopes these credentials are valid for. + final List<String> scopes; + + AccessCredentials( + this.accessToken, + this.refreshToken, + this.scopes, { + this.idToken, + }); + + factory AccessCredentials.fromJson(Map<String, dynamic> json) => + AccessCredentials( + AccessToken.fromJson(json['accessToken'] as Map<String, dynamic>), + json['refreshToken'] as String?, + (json['scopes'] as List<dynamic>).map((e) => e as String).toList(), + idToken: json['idToken'] as String?, + ); + + Map<String, dynamic> toJson() => <String, dynamic>{ + 'accessToken': accessToken, + if (refreshToken != null) 'refreshToken': refreshToken, + 'idToken': idToken, + 'scopes': scopes, + }; +}
diff --git a/googleapis_auth/lib/src/access_token.dart b/googleapis_auth/lib/src/access_token.dart new file mode 100644 index 0000000..16a74ba --- /dev/null +++ b/googleapis_auth/lib/src/access_token.dart
@@ -0,0 +1,43 @@ +// Copyright (c) 2021, 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 OAuth2 access token. +class AccessToken { + /// The token type, usually "Bearer" + final String type; + + /// The access token data. + final String data; + + /// Time at which the token will be expired (UTC time) + final DateTime expiry; + + /// [expiry] must be a UTC `DateTime`. + AccessToken(this.type, this.data, this.expiry) { + if (!expiry.isUtc) { + throw ArgumentError.value( + expiry, + 'expiry', + 'The expiry date must be a Utc DateTime.', + ); + } + } + + factory AccessToken.fromJson(Map<String, dynamic> json) => AccessToken( + json['type'] as String, + json['data'] as String, + DateTime.parse(json['expiry'] as String), + ); + + bool get hasExpired => DateTime.now().toUtc().isAfter(expiry); + + @override + String toString() => 'AccessToken(type=$type, data=$data, expiry=$expiry)'; + + Map<String, dynamic> toJson() => <String, dynamic>{ + 'type': type, + 'data': data, + 'expiry': expiry.toIso8601String(), + }; +}
diff --git a/googleapis_auth/lib/src/adc_utils.dart b/googleapis_auth/lib/src/adc_utils.dart index f11d624..e44b4f5 100644 --- a/googleapis_auth/lib/src/adc_utils.dart +++ b/googleapis_auth/lib/src/adc_utils.dart
@@ -1,11 +1,17 @@ -import 'dart:io'; -import 'dart:convert'; +// Copyright (c) 2021, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + import 'dart:async'; +import 'dart:convert'; +import 'dart:io'; import 'package:http/http.dart'; +import 'auth_functions.dart'; import 'auth_http_utils.dart'; -import '../auth_io.dart'; +import 'service_account_client.dart'; +import 'service_account_credentials.dart'; Future<AutoRefreshingAuthClient> fromApplicationsCredentialsFile( File file, @@ -13,7 +19,7 @@ List<String> scopes, Client baseClient, ) async { - var credentials; + Object? credentials; try { credentials = json.decode(await file.readAsString()); } on IOException { @@ -28,8 +34,8 @@ if (credentials is Map && credentials['type'] == 'authorized_user') { final clientId = ClientId( - credentials['client_id'], - credentials['client_secret'], + credentials['client_id'] as String, + credentials['client_secret'] as String?, ); return AutoRefreshingClient( baseClient, @@ -39,12 +45,12 @@ AccessCredentials( // Hack: Create empty credentials that have expired. AccessToken('Bearer', '', DateTime(0).toUtc()), - credentials['refresh_token'], + credentials['refresh_token'] as String?, scopes, ), baseClient, ), - quotaProject: credentials["quota_project_id"], + quotaProject: credentials['quota_project_id'] as String?, ); } return await clientViaServiceAccount(
diff --git a/googleapis_auth/lib/src/auth_client.dart b/googleapis_auth/lib/src/auth_client.dart new file mode 100644 index 0000000..a581bf2 --- /dev/null +++ b/googleapis_auth/lib/src/auth_client.dart
@@ -0,0 +1,21 @@ +// Copyright (c) 2021, 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'; + +import 'access_credentials.dart'; + +/// A authenticated HTTP client. +abstract class AuthClient implements Client { + /// The credentials currently used for making HTTP requests. + AccessCredentials get credentials; +} + +/// A auto-refreshing, authenticated HTTP client. +abstract class AutoRefreshingAuthClient implements AuthClient { + /// A broadcast stream of [AccessCredentials]. + /// + /// A listener will get notified when new [AccessCredentials] were obtained. + Stream<AccessCredentials> get credentialUpdates; +}
diff --git a/googleapis_auth/lib/src/auth_functions.dart b/googleapis_auth/lib/src/auth_functions.dart new file mode 100644 index 0000000..e267256 --- /dev/null +++ b/googleapis_auth/lib/src/auth_functions.dart
@@ -0,0 +1,127 @@ +// Copyright (c) 2014, 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'; + +import 'access_credentials.dart'; +import 'auth_client.dart'; +import 'auth_http_utils.dart'; +import 'client_id.dart'; +import 'http_client_base.dart'; +import 'utils.dart'; + +/// Obtains a [Client] which uses the given [apiKey] for making HTTP +/// requests. +/// +/// {@macro googleapis_auth_baseClient_param} +/// +/// Note that the returned client should *only* be used for making HTTP requests +/// to Google Services. The [apiKey] should not be disclosed to third parties. +/// +/// {@template googleapis_auth_close_the_client} +/// The user is responsible for closing the returned HTTP [Client]. +/// {@endtemplate} +/// {@template googleapis_auth_not_close_the_baseClient} +/// Closing the returned [Client] will not close [baseClient]. +/// {@endtemplate} +Client clientViaApiKey( + String apiKey, { + Client? baseClient, +}) { + if (baseClient == null) { + baseClient = Client(); + } else { + baseClient = nonClosingClient(baseClient); + } + return ApiKeyClient(baseClient, apiKey); +} + +/// Obtain a [Client] which automatically authenticates requests using +/// [credentials]. +/// +/// Note that the returned [AuthClient] will not auto-refresh the given +/// [credentials]. +/// +/// {@macro googleapis_auth_close_the_client} +/// {@macro googleapis_auth_not_close_the_baseClient} +AuthClient authenticatedClient( + Client baseClient, + AccessCredentials credentials, +) { + if (credentials.accessToken.type != 'Bearer') { + throw ArgumentError('Only Bearer access tokens are accepted.'); + } + return AuthenticatedClient(baseClient, credentials); +} + +/// Creates an [AutoRefreshingAuthClient] which automatically refreshes +/// [credentials] before they expire. +/// +/// {@template googleapis_auth_clientId_param} +/// The [clientId] that you obtain from the API Console +/// [Credentials page](https://console.developers.google.com/apis/credentials), +/// as described in +/// [Obtain OAuth 2.0 credentials](https://developers.google.com/identity/protocols/oauth2/openid-connect#getcredentials). +/// {@endtemplate} +/// +/// Uses [baseClient] to make authenticated HTTP requests and to refresh +/// [credentials]. +/// +/// {@macro googleapis_auth_close_the_client} +/// {@macro googleapis_auth_not_close_the_baseClient} +AutoRefreshingAuthClient autoRefreshingClient( + ClientId clientId, + AccessCredentials credentials, + Client baseClient, +) { + if (credentials.accessToken.type != 'Bearer') { + throw ArgumentError('Only Bearer access tokens are accepted.'); + } + if (credentials.refreshToken == null) { + throw ArgumentError('Refresh token in AccessCredentials was `null`.'); + } + return AutoRefreshingClient(baseClient, clientId, credentials); +} + +/// Obtains refreshed [AccessCredentials] for [clientId] and [credentials]. +/// +/// {@macro googleapis_auth_clientId_param} +/// +/// {@macro googleapis_auth_client_for_creds} +Future<AccessCredentials> refreshCredentials( + ClientId clientId, + AccessCredentials credentials, + Client client, +) async { + final secret = clientId.secret; + if (secret == null) { + throw ArgumentError('clientId.secret cannot be null.'); + } + + final refreshToken = credentials.refreshToken; + if (refreshToken == null) { + throw ArgumentError('clientId.refreshToken cannot be null.'); + } + + // https://developers.google.com/identity/protocols/oauth2/native-app#offline + final jsonMap = await client.oauthTokenRequest({ + 'client_id': clientId.identifier, + 'client_secret': secret, + 'refresh_token': refreshToken, + 'grant_type': 'refresh_token', + }); + + final accessToken = parseAccessToken(jsonMap); + + final idToken = jsonMap['id_token'] as String?; + + return AccessCredentials( + accessToken, + credentials.refreshToken, + credentials.scopes, + idToken: idToken, + ); +}
diff --git a/googleapis_auth/lib/src/auth_http_utils.dart b/googleapis_auth/lib/src/auth_http_utils.dart index 0f1ba58..29a1003 100644 --- a/googleapis_auth/lib/src/auth_http_utils.dart +++ b/googleapis_auth/lib/src/auth_http_utils.dart
@@ -2,39 +2,45 @@ // 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. -library googleapis_auth; - import 'dart:async'; import 'package:http/http.dart'; -import '../auth.dart'; +import 'access_credentials.dart'; +import 'auth_client.dart'; +import 'auth_functions.dart'; +import 'client_id.dart'; +import 'exceptions.dart'; import 'http_client_base.dart'; /// Will close the underlying `http.Client` depending on a constructor argument. class AuthenticatedClient extends DelegatingClient implements AuthClient { + @override final AccessCredentials credentials; - final String quotaProject; + final String? quotaProject; AuthenticatedClient(Client client, this.credentials, {this.quotaProject}) : super(client, closeUnderlyingClient: false); + @override Future<StreamedResponse> send(BaseRequest request) async { // Make new request object and perform the authenticated request. - var modifiedRequest = - new RequestImpl(request.method, request.url, request.finalize()); + final modifiedRequest = + RequestImpl(request.method, request.url, request.finalize()); modifiedRequest.headers.addAll(request.headers); modifiedRequest.headers['Authorization'] = 'Bearer ${credentials.accessToken.data}'; if (quotaProject != null) { - modifiedRequest.headers['X-Goog-User-Project'] = quotaProject; + modifiedRequest.headers['X-Goog-User-Project'] = quotaProject!; } - var response = await baseClient.send(modifiedRequest); - var wwwAuthenticate = response.headers['www-authenticate']; + final response = await baseClient.send(modifiedRequest); + final wwwAuthenticate = response.headers['www-authenticate']; if (wwwAuthenticate != null) { await response.stream.drain(); - throw new AccessDeniedException('Access was denied ' - '(www-authenticate header was: $wwwAuthenticate).'); + throw AccessDeniedException( + 'Access was denied ' + '(www-authenticate header was: $wwwAuthenticate).', + ); } return response; } @@ -52,12 +58,14 @@ : _encodedApiKey = Uri.encodeQueryComponent(apiKey), super(client, closeUnderlyingClient: true); - Future<StreamedResponse> send(BaseRequest request) { + @override + Future<StreamedResponse> send(BaseRequest request) async { var url = request.url; if (url.queryParameters.containsKey('key')) { - return new Future.error(new Exception( - 'Tried to make a HTTP request which has already a "key" query ' - 'parameter. Adding the API key would override that existing value.')); + throw ArgumentError( + 'Tried to make a HTTP request which has already a "key" query ' + 'parameter. Adding the API key would override that existing value.', + ); } if (url.query == '') { @@ -66,9 +74,8 @@ url = url.replace(query: '${url.query}&key=$_encodedApiKey'); } - var modifiedRequest = - new RequestImpl(request.method, url, request.finalize()); - modifiedRequest.headers.addAll(request.headers); + final modifiedRequest = RequestImpl(request.method, url, request.finalize()) + ..headers.addAll(request.headers); return baseClient.send(modifiedRequest); } } @@ -76,15 +83,20 @@ /// Will close the underlying `http.Client` depending on a constructor argument. class AutoRefreshingClient extends AutoRefreshDelegatingClient { final ClientId clientId; - final String quotaProject; + final String? quotaProject; + @override AccessCredentials credentials; - Client authClient; + late Client authClient; - AutoRefreshingClient(Client client, this.clientId, this.credentials, - {bool closeUnderlyingClient: false, this.quotaProject}) - : super(client, closeUnderlyingClient: closeUnderlyingClient) { - assert(credentials.accessToken.type == 'Bearer'); - assert(credentials.refreshToken != null); + AutoRefreshingClient( + Client client, + this.clientId, + this.credentials, { + bool closeUnderlyingClient = true, + this.quotaProject, + }) : assert(credentials.accessToken.type == 'Bearer'), + assert(credentials.refreshToken != null), + super(client, closeUnderlyingClient: closeUnderlyingClient) { authClient = AuthenticatedClient( baseClient, credentials, @@ -92,13 +104,14 @@ ); } + @override Future<StreamedResponse> send(BaseRequest request) async { if (!credentials.accessToken.hasExpired) { // TODO: Can this return a "access token expired" message? // If so, we should handle it. return authClient.send(request); } else { - var cred = await refreshCredentials(clientId, credentials, baseClient); + final cred = await refreshCredentials(clientId, credentials, baseClient); notifyAboutNewCredentials(cred); credentials = cred; authClient = AuthenticatedClient( @@ -114,11 +127,14 @@ abstract class AutoRefreshDelegatingClient extends DelegatingClient implements AutoRefreshingAuthClient { final StreamController<AccessCredentials> _credentialStreamController = - new StreamController.broadcast(sync: true); + StreamController.broadcast(sync: true); - AutoRefreshDelegatingClient(Client client, {bool closeUnderlyingClient: true}) - : super(client, closeUnderlyingClient: closeUnderlyingClient); + AutoRefreshDelegatingClient( + Client client, { + bool closeUnderlyingClient = true, + }) : super(client, closeUnderlyingClient: closeUnderlyingClient); + @override Stream<AccessCredentials> get credentialUpdates => _credentialStreamController.stream; @@ -126,6 +142,7 @@ _credentialStreamController.add(credentials); } + @override void close() { _credentialStreamController.close(); super.close();
diff --git a/googleapis_auth/lib/src/client_id.dart b/googleapis_auth/lib/src/client_id.dart new file mode 100644 index 0000000..4652708 --- /dev/null +++ b/googleapis_auth/lib/src/client_id.dart
@@ -0,0 +1,29 @@ +// Copyright (c) 2021, 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. + +/// Represents the client application's credentials. +class ClientId { + /// The client ID that you obtain from the API Console + /// [Credentials page](https://console.developers.google.com/apis/credentials), + /// as described in + /// [Obtain OAuth 2.0 credentials](https://developers.google.com/identity/protocols/oauth2/openid-connect#getcredentials). + final String identifier; + + /// The client secret used to identify this application to the server. + final String? secret; + + ClientId(this.identifier, [this.secret]); + + ClientId.serviceAccount(this.identifier) : secret = null; + + factory ClientId.fromJson(Map<String, dynamic> json) => ClientId( + json['identifier'] as String, + json['secret'] as String?, + ); + + Map<String, dynamic> toJson() => { + 'identifier': identifier, + if (secret != null) 'secret': secret, + }; +}
diff --git a/googleapis_auth/lib/src/crypto/asn1.dart b/googleapis_auth/lib/src/crypto/asn1.dart index 67defcd..f28b3cf 100644 --- a/googleapis_auth/lib/src/crypto/asn1.dart +++ b/googleapis_auth/lib/src/crypto/asn1.dart
@@ -2,29 +2,28 @@ // 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. -library appengine_auth.asn; - import 'dart:typed_data'; import 'rsa.dart'; +// ignore: avoid_classes_with_only_static_members class ASN1Parser { - static const INTEGER_TAG = 0x02; - static const OCTET_STRING_TAG = 0x04; - static const NULL_TAG = 0x05; - static const OBJECT_ID_TAG = 0x06; - static const SEQUENCE_TAG = 0x30; + static const integerTag = 0x02; + static const octetStringTag = 0x04; + static const nullTag = 0x05; + static const objectIdTag = 0x06; + static const sequenceTag = 0x30; static ASN1Object parse(Uint8List bytes) { - invalidFormat(String msg) { - throw new ArgumentError("Invalid DER encoding: $msg"); + Never invalidFormat(String msg) { + throw ArgumentError('Invalid DER encoding: $msg'); } - var data = new ByteData.view(bytes.buffer); - int offset = 0; - int end = bytes.length; + final data = ByteData.view(bytes.buffer); + var offset = 0; + final end = bytes.length; - checkNBytesAvailable(int n) { + void checkNBytesAvailable(int n) { if ((offset + n) > end) { invalidFormat('Tried to read more bytes than available.'); } @@ -33,7 +32,7 @@ List<int> readBytes(int n) { checkNBytesAvailable(n); - var integerBytes = bytes.sublist(offset, offset + n); + final integerBytes = bytes.sublist(offset, offset + n); offset += n; return integerBytes; } @@ -41,7 +40,7 @@ int readEncodedLength() { checkNBytesAvailable(1); - var lengthByte = data.getUint8(offset++); + final lengthByte = data.getUint8(offset++); // Short length encoding form: This byte is the length itself. if (lengthByte < 0x80) { @@ -51,10 +50,10 @@ // Long length encoding form: // This byte has in bits 0..6 the number of bytes following which encode // the length. - int countLengthBytes = lengthByte & 0x7f; + var countLengthBytes = lengthByte & 0x7f; checkNBytesAvailable(countLengthBytes); - int length = 0; + var length = 0; while (countLengthBytes > 0) { length = (length << 8) | data.getUint8(offset++); countLengthBytes--; @@ -64,7 +63,7 @@ void readNullBytes() { checkNBytesAvailable(1); - var nullByte = data.getUint8(offset++); + final nullByte = data.getUint8(offset++); if (nullByte != 0x00) { invalidFormat('Null byte expect, but was: $nullByte.'); } @@ -72,43 +71,42 @@ ASN1Object decodeObject() { checkNBytesAvailable(1); - var tag = bytes[offset++]; + final tag = bytes[offset++]; switch (tag) { - case INTEGER_TAG: - int size = readEncodedLength(); - return new ASN1Integer(RSAAlgorithm.bytes2BigInt(readBytes(size))); - case OCTET_STRING_TAG: - var size = readEncodedLength(); - return new ASN1OctetString(readBytes(size)); - case NULL_TAG: + case integerTag: + final size = readEncodedLength(); + return ASN1Integer(RSAAlgorithm.bytes2BigInt(readBytes(size))); + case octetStringTag: + final size = readEncodedLength(); + return ASN1OctetString(readBytes(size)); + case nullTag: readNullBytes(); - return new ASN1Null(); - case OBJECT_ID_TAG: - var size = readEncodedLength(); - return new ASN1ObjectIdentifier(readBytes(size)); - case SEQUENCE_TAG: - var lengthInBytes = readEncodedLength(); + return ASN1Null(); + case objectIdTag: + final size = readEncodedLength(); + return ASN1ObjectIdentifier(readBytes(size)); + case sequenceTag: + final lengthInBytes = readEncodedLength(); if ((offset + lengthInBytes) > end) { invalidFormat('Tried to read more bytes than available.'); } - int endOfSequence = offset + lengthInBytes; + final endOfSequence = offset + lengthInBytes; - var objects = <ASN1Object>[]; + final objects = <ASN1Object>[]; while (offset < endOfSequence) { objects.add(decodeObject()); } - return new ASN1Sequence(objects); + return ASN1Sequence(objects); default: invalidFormat( - 'Unexpected tag $tag at offset ${offset - 1} (end: $end).'); + 'Unexpected tag $tag at offset ${offset - 1} (end: $end).', + ); } - // Unreachable. - return null; } - var obj = decodeObject(); + final obj = decodeObject(); if (offset != bytes.length) { - throw new ArgumentError('More bytes than expected in ASN1 encoding.'); + throw ArgumentError('More bytes than expected in ASN1 encoding.'); } return obj; } @@ -118,21 +116,25 @@ class ASN1Sequence extends ASN1Object { final List<ASN1Object> objects; + ASN1Sequence(this.objects); } class ASN1Integer extends ASN1Object { final BigInt integer; + ASN1Integer(this.integer); } class ASN1OctetString extends ASN1Object { final List<int> bytes; + ASN1OctetString(this.bytes); } class ASN1ObjectIdentifier extends ASN1Object { final List<int> bytes; + ASN1ObjectIdentifier(this.bytes); }
diff --git a/googleapis_auth/lib/src/crypto/pem.dart b/googleapis_auth/lib/src/crypto/pem.dart index c3b9041..b000796 100644 --- a/googleapis_auth/lib/src/crypto/pem.dart +++ b/googleapis_auth/lib/src/crypto/pem.dart
@@ -2,8 +2,6 @@ // 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. -library googleapis_auth.pem; - import 'dart:convert'; import 'dart:typed_data'; @@ -16,16 +14,13 @@ /// $ openssl pkcs12 -nocerts -nodes -passin pass:notasecret \ /// -in *-privatekey.p12 -out *-privatekey.pem RSAPrivateKey keyFromString(String pemFileString) { - if (pemFileString == null) { - throw new ArgumentError('Argument must not be null.'); - } - var bytes = _getBytesFromPEMString(pemFileString); + final bytes = _getBytesFromPEMString(pemFileString); return _extractRSAKeyFromDERBytes(bytes); } /// Helper function for decoding the base64 in [pemString]. Uint8List _getBytesFromPEMString(String pemString) { - var lines = LineSplitter.split(pemString) + final lines = LineSplitter.split(pemString) .map((line) => line.trim()) .where((line) => line.isNotEmpty) .toList(); @@ -33,11 +28,13 @@ if (lines.length < 2 || !lines.first.startsWith('-----BEGIN') || !lines.last.startsWith('-----END')) { - throw new ArgumentError('The given string does not have the correct ' - 'begin/end markers expected in a PEM file.'); + throw ArgumentError( + 'The given string does not have the correct ' + 'begin/end markers expected in a PEM file.', + ); } - var base64 = lines.sublist(1, lines.length - 1).join(''); - return new Uint8List.fromList(base64Decode(base64)); + final base64 = lines.sublist(1, lines.length - 1).join(); + return Uint8List.fromList(base64Decode(base64)); } /// Helper to decode the ASN.1/DER bytes in [bytes] into an [RSAPrivateKey]. @@ -55,162 +52,53 @@ // RSAPrivateKey privateKeyFromSequence(ASN1Sequence asnSequence) { - var objects = asnSequence.objects; + final objects = asnSequence.objects; - var asnIntegers = objects.take(9).map((o) => o as ASN1Integer).toList(); + final asnIntegers = objects.take(9).map((o) => o as ASN1Integer).toList(); - var version = asnIntegers.first; + final version = asnIntegers.first; if (version.integer != BigInt.zero) { - throw new ArgumentError('Expected version 0, got: ${version.integer}.'); + throw ArgumentError('Expected version 0, got: ${version.integer}.'); } - var key = new RSAPrivateKey( - asnIntegers[1].integer, - asnIntegers[2].integer, - asnIntegers[3].integer, - asnIntegers[4].integer, - asnIntegers[5].integer, - asnIntegers[6].integer, - asnIntegers[7].integer, - asnIntegers[8].integer); + final key = RSAPrivateKey( + asnIntegers[1].integer, + asnIntegers[2].integer, + asnIntegers[3].integer, + asnIntegers[4].integer, + asnIntegers[5].integer, + asnIntegers[6].integer, + asnIntegers[7].integer, + asnIntegers[8].integer, + ); - var bitLength = key.bitLength; + final bitLength = key.bitLength; if (bitLength != 1024 && bitLength != 2048 && bitLength != 4096) { - throw new ArgumentError('The RSA modulus has a bit length of $bitLength. ' - 'Only 1024, 2048 and 4096 are supported.'); + throw ArgumentError( + 'The RSA modulus has a bit length of $bitLength. ' + 'Only 1024, 2048 and 4096 are supported.', + ); } return key; } try { - var asn = ASN1Parser.parse(bytes); + final asn = ASN1Parser.parse(bytes); if (asn is ASN1Sequence) { - var objects = asn.objects; + final objects = asn.objects; if (objects.length == 3 && objects[2] is ASN1OctetString) { - ASN1OctetString string = objects[2]; + final string = objects[2] as ASN1OctetString; // Seems like the embedded form. // TODO: Validate that rsa identifier matches! - return privateKeyFromSequence(ASN1Parser.parse(string.bytes)); + return privateKeyFromSequence( + ASN1Parser.parse(string.bytes as Uint8List) as ASN1Sequence, + ); } } - return privateKeyFromSequence(asn); + return privateKeyFromSequence(asn as ASN1Sequence); } catch (error) { - throw new ArgumentError( - 'Error while extracting private key from DER bytes: $error'); + throw ArgumentError( + 'Error while extracting private key from DER bytes: $error', + ); } } - -/* - * Example of generating a public/private RSA keypair with 2048 bits and dumping - * the structure of the resulting private key. - - $ openssl genrsa -out key.pem 2048 - Generating RSA private key, 2048 bit long modulus - ..................................................+++ - ..................................................+++ - e is 65537 (0x10001) - - $ cat key.pem - -----BEGIN RSA PRIVATE KEY----- - MIIEowIBAAKCAQEAuDOwXO14ltE1j2O0iDSuqtbw/1kMKjeiki3oehk2zNoUte42 - /s2rX15nYCkKtYG/r8WYvKzb31P4Uow1S4fFydKNWxgX4VtEjHgeqfPxeCL9wiJc - 9KkEt4fyhj1Jo7193gCLtovLAFwPzAMbFLiXWkfqalJ5Z77fOE4Mo7u4pEgxNPgL - VFGe0cEOAsHsKlsze+m1pmPHwWNVTcoKe5o0hOzy6hCPgVc6me6Y7aO8Fb4OVg0l - XQdQpWn2ikVBpzBcZ6InnYyJ/CJNa3WL1LJ65mmYnfHtKGoMqhLK48OReguwRwwF - e9/2+8UcdZcN5rsvt7yg3ZrKNH8rx+wZ36sRewIDAQABAoIBAQCn1HCcOsHkqDlk - rDOQ5m8+uRhbj4bF8GrvRWTL2q1TeF/mY2U4Q6wg+KK3uq1HMzCzthWz0suCb7+R - dq4YY1ySxoSEuy8G5WFPmyJVNy6Lh1Yty6FmSZlCn1sZdD3kMoK8A0NIz5Xmffrm - pu3Fs2ozl9K9jOeQ3xgC9RoPFLrm8lHJ45Vn+SnTxZnsXT6pwpg3TnFIx5ZinU8k - l0Um1n80qD2QQDakQ5jyr2odAELLBDlyCkxAglBXAVt4nk9Kl6nxb4snd9dnrL70 - WjLynWQsDczaV9TZIl2hYkMud+9OLVlUUtB+0c5b0p2t2P0sLltDaq3H6pT6yu2G - 8E86J9IBAoGBAPJaTNV5ysVOFn+YwWwRztzrvNArUJkVq8abN0gGp3gUvDEZnvzK - weF7+lfZzcwVRmQkL3mWLzzZvCx77RfulAzLi5iFuRBPhhhxAPDiDuyL9B7O81G/ - M/W5DPctGOyD/9cnLuh72oij0unc5MLSfzJf8wblpcjJnPBDqIVh6Qt9AoGBAMKT - Gacf4iSj1xW+0wrnbZlDuyCl6Msptj8ePcvLQrFqQmBwsXmWgVR+gFc/1G3lRft0 - QC6chsmafQHIIPpaDjq3sQ01/tUu7LXL+g/Hw9XtUHbkg3sZIQBtC26rKdStfHNS - KTvuCgn/dAJNjiohfhWMt9R4Q6E5FV6PqQHJzPJXAoGAC41qZDKuC8GxKNvrPG+M - 4NML6RBngySZT5pOhExs5zh10BFclshDfbAfOtjTCotpE5T1/mG+VrQ6WBSANMfW - ntWFDfwx2ikwRzH7zX+5HmV9eYp75sWqgGgVyiKIMZ4JMARaJBLjU+gbQbKZ5P+L - uKcCOq3vvSZ/KKTQ/6qvJTECgYBiWgbCgoxF5wdmd4Gn5llw+lqRYyur3hbACuJD - rCe3FDYfF3euNRSEiDkJYTtYnWbldtqmdPpw14VOrEF3KqQ8q/Nz8RIx4jlGn6dz - 6I8mCIH+xv1q8MXMuFHqC9zmIxdgF2y+XVF3wkd6jodI5oscC3g0juHokbkqhkVw - oPfWmwKBgBfR6jv0gWWeWTfkNwj+cMLHQV1uvz6JyLH5K4iISEDFxYkd37jrHB8A - 9hz9UDfmCbSs2j8CXDg7zCayM6tfu4Vtx+8S5g3oN6sa1JXFY1Os7SoXhTfX9M+7 - QpYYDJZwkgZrVQoKMIdCs9xfyVhZERq945NYLekwE1t2W+tOVBgR - -----END RSA PRIVATE KEY----- - - $ openssl enc -d -base64 -in key.pem -out key.bin - - $ dumpasn1 key.bin - 0 1187: SEQUENCE { - 4 1: INTEGER 0 - 7 257: INTEGER - : 00 B8 33 B0 5C ED 78 96 D1 35 8F 63 B4 88 34 AE - : AA D6 F0 FF 59 0C 2A 37 A2 92 2D E8 7A 19 36 CC - : DA 14 B5 EE 36 FE CD AB 5F 5E 67 60 29 0A B5 81 - : BF AF C5 98 BC AC DB DF 53 F8 52 8C 35 4B 87 C5 - : C9 D2 8D 5B 18 17 E1 5B 44 8C 78 1E A9 F3 F1 78 - : 22 FD C2 22 5C F4 A9 04 B7 87 F2 86 3D 49 A3 BD - : 7D DE 00 8B B6 8B CB 00 5C 0F CC 03 1B 14 B8 97 - : 5A 47 EA 6A 52 79 67 BE DF 38 4E 0C A3 BB B8 A4 - : [ Another 129 bytes skipped ] - 268 3: INTEGER 65537 - 273 257: INTEGER - : 00 A7 D4 70 9C 3A C1 E4 A8 39 64 AC 33 90 E6 6F - : 3E B9 18 5B 8F 86 C5 F0 6A EF 45 64 CB DA AD 53 - : 78 5F E6 63 65 38 43 AC 20 F8 A2 B7 BA AD 47 33 - : 30 B3 B6 15 B3 D2 CB 82 6F BF 91 76 AE 18 63 5C - : 92 C6 84 84 BB 2F 06 E5 61 4F 9B 22 55 37 2E 8B - : 87 56 2D CB A1 66 49 99 42 9F 5B 19 74 3D E4 32 - : 82 BC 03 43 48 CF 95 E6 7D FA E6 A6 ED C5 B3 6A - : 33 97 D2 BD 8C E7 90 DF 18 02 F5 1A 0F 14 BA E6 - : [ Another 129 bytes skipped ] - 534 129: INTEGER - : 00 F2 5A 4C D5 79 CA C5 4E 16 7F 98 C1 6C 11 CE - : DC EB BC D0 2B 50 99 15 AB C6 9B 37 48 06 A7 78 - : 14 BC 31 19 9E FC CA C1 E1 7B FA 57 D9 CD CC 15 - : 46 64 24 2F 79 96 2F 3C D9 BC 2C 7B ED 17 EE 94 - : 0C CB 8B 98 85 B9 10 4F 86 18 71 00 F0 E2 0E EC - : 8B F4 1E CE F3 51 BF 33 F5 B9 0C F7 2D 18 EC 83 - : FF D7 27 2E E8 7B DA 88 A3 D2 E9 DC E4 C2 D2 7F - : 32 5F F3 06 E5 A5 C8 C9 9C F0 43 A8 85 61 E9 0B - : [ Another 1 bytes skipped ] - 666 129: INTEGER - : 00 C2 93 19 A7 1F E2 24 A3 D7 15 BE D3 0A E7 6D - : 99 43 BB 20 A5 E8 CB 29 B6 3F 1E 3D CB CB 42 B1 - : 6A 42 60 70 B1 79 96 81 54 7E 80 57 3F D4 6D E5 - : 45 FB 74 40 2E 9C 86 C9 9A 7D 01 C8 20 FA 5A 0E - : 3A B7 B1 0D 35 FE D5 2E EC B5 CB FA 0F C7 C3 D5 - : ED 50 76 E4 83 7B 19 21 00 6D 0B 6E AB 29 D4 AD - : 7C 73 52 29 3B EE 0A 09 FF 74 02 4D 8E 2A 21 7E - : 15 8C B7 D4 78 43 A1 39 15 5E 8F A9 01 C9 CC F2 - : [ Another 1 bytes skipped ] - 798 128: INTEGER - : 0B 8D 6A 64 32 AE 0B C1 B1 28 DB EB 3C 6F 8C E0 - : D3 0B E9 10 67 83 24 99 4F 9A 4E 84 4C 6C E7 38 - : 75 D0 11 5C 96 C8 43 7D B0 1F 3A D8 D3 0A 8B 69 - : 13 94 F5 FE 61 BE 56 B4 3A 58 14 80 34 C7 D6 9E - : D5 85 0D FC 31 DA 29 30 47 31 FB CD 7F B9 1E 65 - : 7D 79 8A 7B E6 C5 AA 80 68 15 CA 22 88 31 9E 09 - : 30 04 5A 24 12 E3 53 E8 1B 41 B2 99 E4 FF 8B B8 - : A7 02 3A AD EF BD 26 7F 28 A4 D0 FF AA AF 25 31 - 929 128: INTEGER - : 62 5A 06 C2 82 8C 45 E7 07 66 77 81 A7 E6 59 70 - : FA 5A 91 63 2B AB DE 16 C0 0A E2 43 AC 27 B7 14 - : 36 1F 17 77 AE 35 14 84 88 39 09 61 3B 58 9D 66 - : E5 76 DA A6 74 FA 70 D7 85 4E AC 41 77 2A A4 3C - : AB F3 73 F1 12 31 E2 39 46 9F A7 73 E8 8F 26 08 - : 81 FE C6 FD 6A F0 C5 CC B8 51 EA 0B DC E6 23 17 - : 60 17 6C BE 5D 51 77 C2 47 7A 8E 87 48 E6 8B 1C - : 0B 78 34 8E E1 E8 91 B9 2A 86 45 70 A0 F7 D6 9B - 1060 128: INTEGER - : 17 D1 EA 3B F4 81 65 9E 59 37 E4 37 08 FE 70 C2 - : C7 41 5D 6E BF 3E 89 C8 B1 F9 2B 88 88 48 40 C5 - : C5 89 1D DF B8 EB 1C 1F 00 F6 1C FD 50 37 E6 09 - : B4 AC DA 3F 02 5C 38 3B CC 26 B2 33 AB 5F BB 85 - : 6D C7 EF 12 E6 0D E8 37 AB 1A D4 95 C5 63 53 AC - : ED 2A 17 85 37 D7 F4 CF BB 42 96 18 0C 96 70 92 - : 06 6B 55 0A 0A 30 87 42 B3 DC 5F C9 58 59 11 1A - : BD E3 93 58 2D E9 30 13 5B 76 5B EB 4E 54 18 11 - : } - */
diff --git a/googleapis_auth/lib/src/crypto/rsa.dart b/googleapis_auth/lib/src/crypto/rsa.dart index c9452e0..0d00be4 100644 --- a/googleapis_auth/lib/src/crypto/rsa.dart +++ b/googleapis_auth/lib/src/crypto/rsa.dart
@@ -5,8 +5,6 @@ // A small part is based on a JavaScript implementation of RSA by Tom Wu // but re-written in dart. -library googleapis_auth.rsa; - import 'dart:typed_data'; /// Represents integers obtained while creating a Public/Private key pair. @@ -39,9 +37,18 @@ int get bitLength => n.bitLength; RSAPrivateKey( - this.n, this.e, this.d, this.p, this.q, this.dmp1, this.dmq1, this.coeff); + this.n, + this.e, + this.d, + this.p, + this.q, + this.dmp1, + this.dmq1, + this.coeff, + ); } +// ignore: avoid_classes_with_only_static_members /// Provides a [encrypt] method for encrypting messages with a [RSAPrivateKey]. abstract class RSAAlgorithm { /// Performs the encryption of [bytes] with the private [key]. @@ -51,9 +58,12 @@ /// The [intendedLength] argument specifies the number of bytes in which the /// result should be encoded. Zero bytes will be used for padding. static List<int> encrypt( - RSAPrivateKey key, List<int> bytes, int intendedLength) { - var message = bytes2BigInt(bytes); - var encryptedMessage = _encryptInteger(key, message); + RSAPrivateKey key, + List<int> bytes, + int intendedLength, + ) { + final message = bytes2BigInt(bytes); + final encryptedMessage = _encryptInteger(key, message); return integer2Bytes(encryptedMessage, intendedLength); } @@ -61,14 +71,13 @@ // The following is equivalent to `_modPow(x, key.d, key.n) but is much // more efficient. It exploits the fact that we have dmp1/dmq1. var xp = _modPow(x % key.p, key.dmp1, key.p); - var xq = _modPow(x % key.q, key.dmq1, key.q); + final xq = _modPow(x % key.q, key.dmq1, key.q); while (xp < xq) { xp += key.p; } return ((((xp - xq) * key.coeff) % key.p) * key.q) + xq; } - // TODO(kevmoo): see if this can be done more efficiently with BigInt static BigInt _modPow(BigInt b, BigInt e, BigInt m) { if (e < BigInt.one) { return BigInt.one; @@ -90,17 +99,17 @@ static BigInt bytes2BigInt(List<int> bytes) { var number = BigInt.zero; for (var i = 0; i < bytes.length; i++) { - number = (number << 8) | new BigInt.from(bytes[i]); + number = (number << 8) | BigInt.from(bytes[i]); } return number; } static List<int> integer2Bytes(BigInt integer, int intendedLength) { if (integer < BigInt.one) { - throw new ArgumentError('Only positive integers are supported.'); + throw ArgumentError('Only positive integers are supported.'); } - var bytes = new Uint8List(intendedLength); - for (int i = bytes.length - 1; i >= 0; i--) { + final bytes = Uint8List(intendedLength); + for (var i = bytes.length - 1; i >= 0; i--) { bytes[i] = (integer & _bigIntFF).toInt(); integer >>= 8; } @@ -108,4 +117,4 @@ } } -final _bigIntFF = new BigInt.from(0xff); +final _bigIntFF = BigInt.from(0xff);
diff --git a/googleapis_auth/lib/src/crypto/rsa_sign.dart b/googleapis_auth/lib/src/crypto/rsa_sign.dart index 309610d..a8ac471 100644 --- a/googleapis_auth/lib/src/crypto/rsa_sign.dart +++ b/googleapis_auth/lib/src/crypto/rsa_sign.dart
@@ -2,8 +2,6 @@ // 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. -library googleapis_auth.rsa_sign; - import 'dart:typed_data'; import 'package:crypto/crypto.dart'; @@ -19,7 +17,7 @@ // NIST sha-256 OID (2 16 840 1 101 3 4 2 1) // See a reference for the encoding here: // http://msdn.microsoft.com/en-us/library/bb540809%28v=vs.85%29.aspx - static const _RSA_SHA256_ALGORITHM_IDENTIFIER = const [ + static const _rsaSha256AlgorithmIdentifier = [ 0x06, 0x09, 0x60, @@ -38,11 +36,11 @@ RS256Signer(this._rsaKey); List<int> sign(List<int> bytes) { - var digest = _digestInfo(sha256.convert(bytes).bytes); - var modulusLen = (_rsaKey.bitLength + 7) ~/ 8; + final digest = _digestInfo(sha256.convert(bytes).bytes); + final modulusLen = (_rsaKey.bitLength + 7) ~/ 8; - var block = new Uint8List(modulusLen); - var padLength = block.length - digest.length - 3; + final block = Uint8List(modulusLen); + final padLength = block.length - digest.length - 3; block[0] = 0x00; block[1] = 0x01; block.fillRange(2, 2 + padLength, 0xFF); @@ -57,22 +55,23 @@ // digest OCTET STRING // } var offset = 0; - var digestInfo = new Uint8List( - 2 + 2 + _RSA_SHA256_ALGORITHM_IDENTIFIER.length + 2 + 2 + hash.length); + final digestInfo = Uint8List( + 2 + 2 + _rsaSha256AlgorithmIdentifier.length + 2 + 2 + hash.length, + ); { // DigestInfo - digestInfo[offset++] = ASN1Parser.SEQUENCE_TAG; + digestInfo[offset++] = ASN1Parser.sequenceTag; digestInfo[offset++] = digestInfo.length - 2; { // AlgorithmIdentifier. - digestInfo[offset++] = ASN1Parser.SEQUENCE_TAG; - digestInfo[offset++] = _RSA_SHA256_ALGORITHM_IDENTIFIER.length + 2; - digestInfo.setAll(offset, _RSA_SHA256_ALGORITHM_IDENTIFIER); - offset += _RSA_SHA256_ALGORITHM_IDENTIFIER.length; - digestInfo[offset++] = ASN1Parser.NULL_TAG; + digestInfo[offset++] = ASN1Parser.sequenceTag; + digestInfo[offset++] = _rsaSha256AlgorithmIdentifier.length + 2; + digestInfo.setAll(offset, _rsaSha256AlgorithmIdentifier); + offset += _rsaSha256AlgorithmIdentifier.length; + digestInfo[offset++] = ASN1Parser.nullTag; digestInfo[offset++] = 0; } - digestInfo[offset++] = ASN1Parser.OCTET_STRING_TAG; + digestInfo[offset++] = ASN1Parser.octetStringTag; digestInfo[offset++] = hash.length; digestInfo.setAll(offset, hash); }
diff --git a/googleapis_auth/lib/src/exceptions.dart b/googleapis_auth/lib/src/exceptions.dart new file mode 100644 index 0000000..59b8a7c --- /dev/null +++ b/googleapis_auth/lib/src/exceptions.dart
@@ -0,0 +1,60 @@ +// Copyright (c) 2021, 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. + +/// No longer used. Replaced by [ServerRequestFailedException]. +@Deprecated('No longer used. Replaced by ServerRequestFailedException.') +typedef RefreshFailedException = ServerRequestFailedException; + +/// Thrown if an attempt to make an authorized request failed. +class AccessDeniedException implements Exception { + final String message; + + AccessDeniedException(this.message); + + @override + String toString() => message; +} + +/// Thrown if user did not give his consent. +class UserConsentException implements Exception { + final String message; + + final String? details; + + UserConsentException(this.message, {this.details}); + + @override + String toString() => [message, if (details != null) details].join(' '); +} + +/// Thrown when a request to or the response from an authentication service is +/// invalid. +/// +/// This could indicate invalid credentials. +class ServerRequestFailedException implements Exception { + /// Describes the failure. + final String message; + + /// The HTTP status code of the response, if known. + /// + /// If `null`, the status code was likely `200` and there was another issue + /// with the response. + final int? statusCode; + + /// Data representing the content of the response, if any. + /// + /// This may be a [String] representing the raw content of the response or + /// the a parsed JSON literal of the content. + final Object? responseContent; + + ServerRequestFailedException( + this.message, { + this.statusCode, + required this.responseContent, + }); + + @override + String toString() => + [message, if (statusCode != null) 'Status code: $statusCode'].join(' '); +}
diff --git a/googleapis_auth/lib/src/http_client_base.dart b/googleapis_auth/lib/src/http_client_base.dart index b37bb9b..c2c970a 100644 --- a/googleapis_auth/lib/src/http_client_base.dart +++ b/googleapis_auth/lib/src/http_client_base.dart
@@ -2,8 +2,6 @@ // 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. -library googleapis_auth.http_client_base; - import 'dart:async'; import 'package:http/http.dart'; @@ -17,11 +15,12 @@ final bool closeUnderlyingClient; bool _isClosed = false; - DelegatingClient(this.baseClient, {this.closeUnderlyingClient: true}); + DelegatingClient(this.baseClient, {this.closeUnderlyingClient = true}); + @override void close() { if (_isClosed) { - throw new StateError('Cannot close a HTTP client more than once.'); + throw StateError('Cannot close a HTTP client more than once.'); } _isClosed = true; super.close(); @@ -40,15 +39,11 @@ class RefCountedClient extends DelegatingClient { int _refCount; - RefCountedClient(Client baseClient, {int initialRefCount: 1}) + RefCountedClient(Client baseClient, {int initialRefCount = 1}) : _refCount = initialRefCount, - super(baseClient, closeUnderlyingClient: true) { - if (_refCount == null || _refCount <= 0) { - throw new ArgumentError( - 'A reference count of $initialRefCount is invalid.'); - } - } + super(baseClient, closeUnderlyingClient: true); + @override Future<StreamedResponse> send(BaseRequest request) { _ensureClientIsOpen(); return baseClient.send(request); @@ -73,15 +68,17 @@ } /// Is equivalent to calling `release`. + @override void close() { release(); } void _ensureClientIsOpen() { if (_refCount <= 0) { - throw new StateError( - 'This reference counted HTTP client has reached a count of zero and ' - 'can no longer be used for making HTTP requests.'); + throw StateError( + 'This reference counted HTTP client has reached a count of zero and ' + 'can no longer be used for making HTTP requests.', + ); } } } @@ -90,20 +87,18 @@ // Calling close on the returned client once will not close the underlying // [baseClient]. Client nonClosingClient(Client baseClient) => - new RefCountedClient(baseClient, initialRefCount: 2); + RefCountedClient(baseClient, initialRefCount: 2); class RequestImpl extends BaseRequest { final Stream<List<int>> _stream; - RequestImpl(String method, Uri url, [Stream<List<int>> stream]) - : _stream = stream == null ? new Stream.fromIterable([]) : stream, + RequestImpl(String method, Uri url, [Stream<List<int>>? stream]) + : _stream = stream ?? const Stream.empty(), super(method, url); + @override ByteStream finalize() { super.finalize(); - if (_stream == null) { - return null; - } - return new ByteStream(_stream); + return ByteStream(_stream); } }
diff --git a/googleapis_auth/lib/src/known_uris.dart b/googleapis_auth/lib/src/known_uris.dart new file mode 100644 index 0000000..bc73ddb --- /dev/null +++ b/googleapis_auth/lib/src/known_uris.dart
@@ -0,0 +1,12 @@ +// Copyright (c) 2021, 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. + +/// token_endpoint +/// via https://accounts.google.com/.well-known/openid-configuration +final googleOauth2TokenEndpoint = Uri.https('oauth2.googleapis.com', 'token'); + +/// authorization_endpoint +/// via https://accounts.google.com/.well-known/openid-configuration +final googleOauth2AuthorizationEndpoint = + Uri.https('accounts.google.com', 'o/oauth2/v2/auth');
diff --git a/googleapis_auth/lib/src/metadata_server_client.dart b/googleapis_auth/lib/src/metadata_server_client.dart new file mode 100644 index 0000000..302ce55 --- /dev/null +++ b/googleapis_auth/lib/src/metadata_server_client.dart
@@ -0,0 +1,46 @@ +// Copyright (c) 2021, 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'; + +import 'access_credentials.dart'; +import 'auth_client.dart'; +import 'oauth2_flows/base_flow.dart'; +import 'oauth2_flows/metadata_server.dart'; + +/// Obtain oauth2 [AccessCredentials] using the metadata API on ComputeEngine. +/// +/// In case the VM was not configured with access to the requested scopes or an +/// error occurs the returned future will complete with an `Exception`. +/// +/// {@template googleapis_auth_client_for_creds} +/// [client] will be used for making the HTTP requests needed to create the +/// returned [AccessCredentials]. +/// {@endtemplate} +/// +/// No credentials are needed. But this function is only intended to work on a +/// Google Compute Engine VM with configured access to Google APIs. +Future<AccessCredentials> obtainAccessCredentialsViaMetadataServer( + Client client, +) => + MetadataServerAuthorizationFlow(client).run(); + +/// Obtains oauth2 credentials and returns an authenticated HTTP client. +/// +/// See [obtainAccessCredentialsViaMetadataServer] for specifics about the +/// arguments used for obtaining access credentials. +/// +/// {@macro googleapis_auth_returned_auto_refresh_client} +/// +/// {@macro googleapis_auth_baseClient_param} +/// +/// {@macro googleapis_auth_close_the_client} +/// {@macro googleapis_auth_not_close_the_baseClient} +Future<AutoRefreshingAuthClient> clientViaMetadataServer({ + Client? baseClient, +}) async => + await clientFromFlow( + (c) => MetadataServerAuthorizationFlow(c), + baseClient: baseClient, + );
diff --git a/googleapis_auth/lib/src/oauth2_flows/auth_code.dart b/googleapis_auth/lib/src/oauth2_flows/auth_code.dart index 4cc486c..5d21427 100644 --- a/googleapis_auth/lib/src/oauth2_flows/auth_code.dart +++ b/googleapis_auth/lib/src/oauth2_flows/auth_code.dart
@@ -2,268 +2,155 @@ // 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. -library googleapis_auth.auth_code_flow; - import 'dart:async'; import 'dart:convert'; -import 'dart:io'; +import 'dart:math'; +import 'dart:typed_data'; +import 'package:crypto/crypto.dart'; import 'package:http/http.dart' as http; -import '../../auth.dart'; -import '../http_client_base.dart'; -import '../typedefs.dart'; +import '../access_credentials.dart'; +import '../client_id.dart'; +import '../exceptions.dart'; +import '../known_uris.dart'; import '../utils.dart'; -// The OAuth2 Token endpoint can be used to make requests as -// https://www.googleapis.com/oauth2/v2/tokeninfo?access_token=<token> -// -// A successfull response from the server will give an HTTP response status -// 200 and a body of the following type: -// { -// "issued_to": "XYZ.apps.googleusercontent.com", -// "audience": "XYZ.apps.googleusercontent.com", -// "scope": "https://www.googleapis.com/auth/bigquery", -// "expires_in": 3547, -// "access_type": "offline" -// } -// -// Scopes are separated by spaces. -Future<List<String>> obtainScopesFromAccessToken( - String accessToken, http.Client client) async { - var url = Uri.parse('https://www.googleapis.com/oauth2/v2/tokeninfo' - '?access_token=${Uri.encodeQueryComponent(accessToken)}'); - - var response = await client.post(url); - if (response.statusCode == 200) { - Map json = jsonDecode(response.body); - var scope = json['scope']; - if (scope is! String) { - throw new Exception( - 'The response did not include a `scope` value of type `String`.'); - } - return scope.split(' ').toList(); - } else { - throw new Exception('Unable to obtain list of scopes an access token ' - 'is valid for. Server responded with ${response.statusCode}.'); - } +Uri createAuthenticationUri({ + required String redirectUri, + required String clientId, + required Iterable<String> scopes, + required String codeVerifier, + String? hostedDomain, + String? state, + bool offline = false, +}) { + final queryValues = { + 'client_id': clientId, + 'response_type': 'code', + 'redirect_uri': redirectUri, + 'scope': scopes.join(' '), + 'code_challenge': _codeVerifierShaEncode(codeVerifier), + 'code_challenge_method': 'S256', + if (offline) 'access_type': 'offline', + if (hostedDomain != null) 'hd': hostedDomain, + if (state != null) 'state': state, + }; + return googleOauth2AuthorizationEndpoint.replace( + queryParameters: queryValues, + ); } -Future<AccessCredentials> obtainAccessCredentialsUsingCode( - ClientId clientId, String code, String redirectUrl, http.Client client, - [List<String> scopes]) async { - var uri = Uri.parse('https://accounts.google.com/o/oauth2/token'); - var formValues = [ - 'grant_type=authorization_code', - 'code=${Uri.encodeQueryComponent(code)}', - 'redirect_uri=${Uri.encodeQueryComponent(redirectUrl)}', - 'client_id=${Uri.encodeQueryComponent(clientId.identifier)}', - 'client_secret=${Uri.encodeQueryComponent(clientId.secret)}', - ]; +/// https://developers.google.com/identity/protocols/oauth2/native-app#create-code-challenge +/// https://datatracker.ietf.org/doc/html/rfc7636#section-4.1 +String createCodeVerifier() { + final rnd = Random.secure(); - var body = new Stream<List<int>>.fromIterable( - <List<int>>[ascii.encode(formValues.join('&'))]); - var request = new RequestImpl('POST', uri, body); - request.headers['content-type'] = CONTENT_TYPE_URLENCODED; - - var response = await client.send(request); - Map jsonMap = - await utf8.decoder.bind(response.stream).transform(json.decoder).first; - - var idToken = jsonMap['id_token']; - var tokenType = jsonMap['token_type']; - var accessToken = jsonMap['access_token']; - var seconds = jsonMap['expires_in']; - var refreshToken = jsonMap['refresh_token']; - var error = jsonMap['error']; - - if (response.statusCode != 200 && error != null) { - throw new Exception('Failed to exchange authorization code. ' - 'Response was ${response.statusCode}. Error message was $error.'); - } - - if (response.statusCode != 200 || - accessToken == null || - seconds is! int || - tokenType != 'Bearer') { - throw new Exception('Failed to exchange authorization code. ' - 'Invalid server response. ' - 'Http status code was: ${response.statusCode}.'); - } - - if (scopes != null) { - return new AccessCredentials( - new AccessToken('Bearer', accessToken, expiryDate(seconds)), - refreshToken, - scopes, - idToken: idToken); - } - - scopes = await obtainScopesFromAccessToken(accessToken, client); - return new AccessCredentials( - new AccessToken('Bearer', accessToken, expiryDate(seconds)), - refreshToken, - scopes, - idToken: idToken); + return List.generate(128, (index) => _safe[rnd.nextInt(_safe.length)]).join(); } -/// Abstract class for obtaining access credentials via the authorization code -/// grant flow -/// -/// See -/// * [AuthorizationCodeGrantServerFlow] -/// * [AuthorizationCodeGrantManualFlow] -/// for further details. -abstract class AuthorizationCodeGrantAbstractFlow { - final ClientId clientId; - final List<String> scopes; - final http.Client _client; +/// See https://developers.google.com/identity/protocols/oauth2/openid-connect#createxsrftoken +String randomState() { + final rnd = Random.secure(); - AuthorizationCodeGrantAbstractFlow(this.clientId, this.scopes, this._client); - - Future<AccessCredentials> run(); - - Future<AccessCredentials> _obtainAccessCredentialsUsingCode( - String code, String redirectUri) { - return obtainAccessCredentialsUsingCode( - clientId, code, redirectUri, _client, scopes); + final list = Uint32List(6); + for (var i = 0; i < list.length; i++) { + list[i] = rnd.nextInt(1 << 32); } - String _authenticationUri(String redirectUri, {String state}) { - // TODO: Increase scopes with [include_granted_scopes]. - var queryValues = [ - 'response_type=code', - 'client_id=${Uri.encodeQueryComponent(clientId.identifier)}', - 'redirect_uri=${Uri.encodeQueryComponent(redirectUri)}', - 'scope=${Uri.encodeQueryComponent(scopes.join(' '))}', - ]; - if (state != null) { - queryValues.add('state=${Uri.encodeQueryComponent(state)}'); - } - return Uri.parse('https://accounts.google.com/o/oauth2/auth' - '?${queryValues.join('&')}') - .toString(); - } + final value = base64UrlEncode(Uint8List.view(list.buffer)); + return _stripBase64Equals(value); } -/// Runs an oauth2 authorization code grant flow using an HTTP server. -/// -/// This class is able to run an oauth2 authorization flow. It takes a user -/// supplied function which will be called with an URI. The user is expected -/// to navigate to that URI and to grant access to the client. -/// -/// Once the user has granted access to the client, Google will redirect the -/// user agent to a URL pointing to a locally running HTTP server. Which in turn -/// will be able to extract the authorization code from the URL and use it to -/// obtain access credentials. -class AuthorizationCodeGrantServerFlow - extends AuthorizationCodeGrantAbstractFlow { - final PromptUserForConsent userPrompt; +// https://datatracker.ietf.org/doc/html/rfc3986#section-2.3 +const _safe = '0123456789-._~' + 'abcdefghijklmnopqrstuvwxyz' + 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'; - AuthorizationCodeGrantServerFlow(ClientId clientId, List<String> scopes, - http.Client client, this.userPrompt) - : super(clientId, scopes, client); - - Future<AccessCredentials> run() async { - HttpServer server = await HttpServer.bind('localhost', 0); - - try { - var port = server.port; - var redirectionUri = 'http://localhost:$port'; - var state = 'authcodestate${new DateTime.now().millisecondsSinceEpoch}'; - - // Prompt user and wait until he goes to URL and the google authorization - // server calls back to our locally running HTTP server. - userPrompt(_authenticationUri(redirectionUri, state: state)); - - var request = await server.first; - var uri = request.uri; - - try { - var returnedState = uri.queryParameters['state']; - var code = uri.queryParameters['code']; - var error = uri.queryParameters['error']; - - if (request.method != 'GET') { - throw new Exception('Invalid response from server ' - '(expected GET request callback, got: ${request.method}).'); - } - - if (state != returnedState) { - throw new Exception( - 'Invalid response from server (state did not match).'); - } - - if (error != null) { - throw new UserConsentException( - 'Error occured while obtaining access credentials: $error'); - } - - if (code == null || code == '') { - throw new Exception( - 'Invalid response from server (no auth code transmitted).'); - } - var credentials = - await _obtainAccessCredentialsUsingCode(code, redirectionUri); - - // TODO: We could introduce a user-defined redirect page. - request.response - ..statusCode = 200 - ..headers.set('content-type', 'text/html; charset=UTF-8') - ..write(''' -<!DOCTYPE html> - -<html> - <head> - <meta charset="utf-8"> - <title>Authorization successful.</title> - </head> - - <body> - <h2 style="text-align: center">Application has successfully obtained access credentials</h2> - <p style="text-align: center">This window can be closed now.</p> - </body> -</html>'''); - await request.response.close(); - return credentials; - } catch (e) { - request.response.statusCode = 500; - await request.response.close().catchError((_) {}); - rethrow; - } - } finally { - await server.close(); - } - } +String _codeVerifierShaEncode(String codeVerifier) { + final asciiBytes = ascii.encode(codeVerifier); + final sha26Bytes = sha256.convert(asciiBytes).bytes; + final output = base64UrlEncode(sha26Bytes); + return _stripBase64Equals(output); } -/// Runs an oauth2 authorization code grant flow using manual Copy&Paste. -/// -/// This class is able to run an oauth2 authorization flow. It takes a user -/// supplied function which will be called with an URI. The user is expected -/// to navigate to that URI and to grant access to the client. -/// -/// Google will give the resource owner a code. The user supplied function needs -/// to complete with that code. -/// -/// The authorization code will then be used to obtain access credentials. -class AuthorizationCodeGrantManualFlow - extends AuthorizationCodeGrantAbstractFlow { - final PromptUserForConsentManual userPrompt; - - AuthorizationCodeGrantManualFlow(ClientId clientId, List<String> scopes, - http.Client client, this.userPrompt) - : super(clientId, scopes, client); - - Future<AccessCredentials> run() async { - var redirectionUri = 'urn:ietf:wg:oauth:2.0:oob'; - - // Prompt user and wait until he goes to URL and copy&pastes the auth code - // in. - var code = await userPrompt(_authenticationUri(redirectionUri)); - // Use code to obtain credentials - return _obtainAccessCredentialsUsingCode(code, redirectionUri); +String _stripBase64Equals(String value) { + while (value.endsWith('=')) { + value = value.substring(0, value.length - 1); } + return value; } -// TODO: Server app flow is missing here. +/// Obtain oauth2 [AccessCredentials] by exchanging an authorization code. +/// +/// Running a hybrid oauth2 flow as described in the +/// `googleapis_auth.auth_browser` library results in a `HybridFlowResult` which +/// contains short-lived [AccessCredentials] for the client and an authorization +/// code. This authorization code needs to be transferred to the server, which +/// can exchange it against long-lived [AccessCredentials]. +/// +/// {@macro googleapis_auth_client_for_creds} +/// +/// {@macro googleapis_auth_clientId_param} +/// +/// If the authorization code was obtained using the mentioned hybrid flow, the +/// [redirectUrl] must be `"postmessage"` (default). +/// +/// If you obtained the authorization code using a different mechanism, the +/// [redirectUrl] must be the same that was used to obtain the code. +/// +/// NOTE: Only the server application will know the `client secret` - which is +/// necessary to exchange an authorization code against access tokens. +/// +/// NOTE: It is important to transmit the authorization code in a secure manner +/// to the server. You should use "anti-request forgery state tokens" to guard +/// against "cross site request forgery" attacks. +Future<AccessCredentials> obtainAccessCredentialsViaCodeExchange( + http.Client client, + ClientId clientId, + String code, { + String redirectUrl = 'postmessage', + String? codeVerifier, +}) async { + final jsonMap = await client.oauthTokenRequest( + { + 'client_id': clientId.identifier, + 'client_secret': clientId.secret ?? '', + 'code': code, + if (codeVerifier != null) 'code_verifier': codeVerifier, + 'grant_type': 'authorization_code', + 'redirect_uri': redirectUrl, + }, + ); + final accessToken = parseAccessToken(jsonMap); + + final idToken = jsonMap['id_token'] as String?; + final refreshToken = jsonMap['refresh_token'] as String?; + + final scope = jsonMap['scope']; + if (scope is! String) { + throw ServerRequestFailedException( + 'The response did not include a `scope` value of type `String`.', + responseContent: json, + ); + } + final scopes = scope.split(' ').toList(); + + return AccessCredentials( + accessToken, + refreshToken, + scopes, + idToken: idToken, + ); +} + +List<String> parseScopes(Map<String, dynamic> json) { + final scope = json['scope']; + if (scope is! String) { + throw ServerRequestFailedException( + 'The response did not include a `scope` value of type `String`.', + responseContent: json, + ); + } + return scope.split(' ').toList(); +}
diff --git a/googleapis_auth/lib/src/oauth2_flows/authorization_code_grant_abstract_flow.dart b/googleapis_auth/lib/src/oauth2_flows/authorization_code_grant_abstract_flow.dart new file mode 100644 index 0000000..350b519 --- /dev/null +++ b/googleapis_auth/lib/src/oauth2_flows/authorization_code_grant_abstract_flow.dart
@@ -0,0 +1,51 @@ +// Copyright (c) 2021, 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 '../access_credentials.dart'; +import '../client_id.dart'; +import 'auth_code.dart'; +import 'base_flow.dart'; + +abstract class AuthorizationCodeGrantAbstractFlow implements BaseFlow { + final ClientId clientId; + final String? hostedDomain; + final List<String> scopes; + final http.Client _client; + + AuthorizationCodeGrantAbstractFlow( + this.clientId, + this.scopes, + this._client, { + this.hostedDomain, + }); + + Future<AccessCredentials> obtainAccessCredentialsUsingCodeImpl( + String code, + String redirectUri, { + required String codeVerifier, + }) => + obtainAccessCredentialsViaCodeExchange( + _client, + clientId, + code, + redirectUrl: redirectUri, + codeVerifier: codeVerifier, + ); + + Uri authenticationUri( + String redirectUri, { + String? state, + required String codeVerifier, + }) => + createAuthenticationUri( + redirectUri: redirectUri, + clientId: clientId.identifier, + scopes: scopes, + codeVerifier: codeVerifier, + hostedDomain: hostedDomain, + state: state, + ); +}
diff --git a/googleapis_auth/lib/src/oauth2_flows/authorization_code_grant_manual_flow.dart b/googleapis_auth/lib/src/oauth2_flows/authorization_code_grant_manual_flow.dart new file mode 100644 index 0000000..b8ee3f1 --- /dev/null +++ b/googleapis_auth/lib/src/oauth2_flows/authorization_code_grant_manual_flow.dart
@@ -0,0 +1,58 @@ +// Copyright (c) 2021, 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 '../access_credentials.dart'; +import '../client_id.dart'; +import '../typedefs.dart'; +import 'auth_code.dart'; +import 'authorization_code_grant_abstract_flow.dart'; + +/// Runs an oauth2 authorization code grant flow using manual Copy&Paste. +/// +/// This class is able to run an oauth2 authorization flow. It takes a user +/// supplied function which will be called with an URI. The user is expected +/// to navigate to that URI and to grant access to the client. +/// +/// Google will give the resource owner a code. The user supplied function needs +/// to complete with that code. +/// +/// The authorization code will then be used to obtain access credentials. +class AuthorizationCodeGrantManualFlow + extends AuthorizationCodeGrantAbstractFlow { + final PromptUserForConsentManual userPrompt; + + AuthorizationCodeGrantManualFlow( + ClientId clientId, + List<String> scopes, + http.Client client, + this.userPrompt, { + String? hostedDomain, + }) : super(clientId, scopes, client, hostedDomain: hostedDomain); + + @override + Future<AccessCredentials> run() async { + final codeVerifier = createCodeVerifier(); + + // Prompt user and wait until they goes to URL and copy&pastes the auth code + // in. + final code = await userPrompt( + authenticationUri( + _redirectionUri, + codeVerifier: codeVerifier, + ).toString(), + ); + // Use code to obtain credentials + return obtainAccessCredentialsUsingCodeImpl( + code, + _redirectionUri, + codeVerifier: codeVerifier, + ); + } +} + +const _redirectionUri = 'urn:ietf:wg:oauth:2.0:oob';
diff --git a/googleapis_auth/lib/src/oauth2_flows/authorization_code_grant_server_flow.dart b/googleapis_auth/lib/src/oauth2_flows/authorization_code_grant_server_flow.dart new file mode 100644 index 0000000..95f5b29 --- /dev/null +++ b/googleapis_auth/lib/src/oauth2_flows/authorization_code_grant_server_flow.dart
@@ -0,0 +1,127 @@ +// Copyright (c) 2021, 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' as http; + +import '../access_credentials.dart'; +import '../client_id.dart'; +import '../exceptions.dart'; +import '../typedefs.dart'; +import 'auth_code.dart'; +import 'authorization_code_grant_abstract_flow.dart'; + +/// Runs an oauth2 authorization code grant flow using an HTTP server. +/// +/// This class is able to run an oauth2 authorization flow. It takes a user +/// supplied function which will be called with an URI. The user is expected +/// to navigate to that URI and to grant access to the client. +/// +/// Once the user has granted access to the client, Google will redirect the +/// user agent to a URL pointing to a locally running HTTP server. Which in turn +/// will be able to extract the authorization code from the URL and use it to +/// obtain access credentials. +class AuthorizationCodeGrantServerFlow + extends AuthorizationCodeGrantAbstractFlow { + final PromptUserForConsent userPrompt; + + AuthorizationCodeGrantServerFlow( + ClientId clientId, + List<String> scopes, + http.Client client, + this.userPrompt, { + String? hostedDomain, + }) : super(clientId, scopes, client, hostedDomain: hostedDomain); + + @override + Future<AccessCredentials> run() async { + final server = await HttpServer.bind('localhost', 0); + + try { + final port = server.port; + final redirectionUri = 'http://localhost:$port'; + final state = randomState(); + final codeVerifier = createCodeVerifier(); + + // Prompt user and wait until they goes to URL and the google + // authorization server calls back to our locally running HTTP server. + userPrompt( + authenticationUri( + redirectionUri, + state: state, + codeVerifier: codeVerifier, + ).toString(), + ); + + final request = await server.first; + final uri = request.uri; + + try { + if (request.method != 'GET') { + throw Exception( + 'Invalid response from server ' + '(expected GET request callback, got: ${request.method}).', + ); + } + + final returnedState = uri.queryParameters['state']; + if (state != returnedState) { + throw Exception( + 'Invalid response from server (state did not match).', + ); + } + + final error = uri.queryParameters['error']; + if (error != null) { + throw UserConsentException( + 'Error occurred while obtaining access credentials: $error', + ); + } + + final code = uri.queryParameters['code']; + if (code == null || code.isEmpty) { + throw Exception( + 'Invalid response from server (no auth code transmitted).', + ); + } + final credentials = await obtainAccessCredentialsUsingCodeImpl( + code, + redirectionUri, + codeVerifier: codeVerifier, + ); + + // TODO: We could introduce a user-defined redirect page. + request.response + ..statusCode = 200 + ..headers.set('content-type', 'text/html; charset=UTF-8') + ..write( + ''' +<!DOCTYPE html> + +<html> + <head> + <meta charset="utf-8"> + <title>Authorization successful.</title> + </head> + + <body> + <h2 style="text-align: center">Application has successfully obtained access credentials</h2> + <p style="text-align: center">This window can be closed now.</p> + </body> +</html>''', + ); + await request.response.close(); + return credentials; + } catch (e) { + request.response.statusCode = 500; + await request.response.close().catchError((_) {}); + rethrow; + } + } finally { + await server.close(); + } + } +}
diff --git a/googleapis_auth/lib/src/oauth2_flows/base_flow.dart b/googleapis_auth/lib/src/oauth2_flows/base_flow.dart new file mode 100644 index 0000000..7408d3b --- /dev/null +++ b/googleapis_auth/lib/src/oauth2_flows/base_flow.dart
@@ -0,0 +1,56 @@ +import 'package:http/http.dart'; + +import '../access_credentials.dart'; +import '../auth_client.dart'; +import '../auth_functions.dart'; +import '../auth_http_utils.dart'; +import '../http_client_base.dart'; + +/// Base class for "Flows" that provide [AccessCredentials]. +abstract class BaseFlow { + Future<AccessCredentials> run(); +} + +Future<AutoRefreshingAuthClient> clientFromFlow( + BaseFlow Function(Client client) flowFactory, { + Client? baseClient, +}) async { + if (baseClient == null) { + baseClient = Client(); + } else { + baseClient = nonClosingClient(baseClient); + } + + final flow = flowFactory(baseClient); + + try { + final credentials = await flow.run(); + return _FlowClient(baseClient, credentials, flow); + } catch (e) { + baseClient.close(); + rethrow; + } +} + +// Will close the underlying `http.Client`. +class _FlowClient extends AutoRefreshDelegatingClient { + final BaseFlow _flow; + @override + AccessCredentials credentials; + Client _authClient; + + _FlowClient(Client client, this.credentials, this._flow) + : _authClient = authenticatedClient(client, credentials), + super(client); + + @override + Future<StreamedResponse> send(BaseRequest request) async { + if (credentials.accessToken.hasExpired) { + final newCredentials = await _flow.run(); + notifyAboutNewCredentials(newCredentials); + credentials = newCredentials; + _authClient = authenticatedClient(baseClient, credentials); + } + return _authClient.send(request); + } +}
diff --git a/googleapis_auth/lib/src/oauth2_flows/implicit.dart b/googleapis_auth/lib/src/oauth2_flows/implicit.dart index a90dae6..69cf6bd 100644 --- a/googleapis_auth/lib/src/oauth2_flows/implicit.dart +++ b/googleapis_auth/lib/src/oauth2_flows/implicit.dart
@@ -2,21 +2,21 @@ // 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. -library googleapis_auth.implicit_gapi_flow; - -import "dart:async"; +import 'dart:async'; import 'dart:html' as html; -import "dart:js" as js; +import 'dart:js' as js; -import '../../auth.dart'; -import '../utils.dart'; +import '../access_credentials.dart'; +import '../access_token.dart'; +import '../exceptions.dart'; +import '../response_type.dart'; // This will be overridden by tests. String gapiUrl = 'https://apis.google.com/js/client.js'; // According to the CSP3 spec a nonce must be a valid base64 string. // https://w3c.github.io/webappsec-csp/#grammardef-base64-value -final _noncePattern = new RegExp('^[\\w+\/_-]+[=]{0,2}\$'); +final _noncePattern = RegExp('^[\\w+\/_-]+[=]{0,2}\$'); /// This class performs the implicit browser-based oauth2 flow. /// @@ -38,10 +38,11 @@ /// => Completes with a tuple [AccessCredentials cred, String authCode] /// or an Exception. class ImplicitFlow { - static const CallbackTimeout = const Duration(seconds: 20); + static const callbackTimeout = Duration(seconds: 20); final String _clientId; final List<String> _scopes; + final bool _enableDebugLogs; /// The pending result of an earlier call to [initialize], if any. /// @@ -49,75 +50,76 @@ /// but the gapi JS library should only ever be loaded once. If /// it's called again while a previous initialization is still pending, /// this will be returned. - static Future<void> _pendingInitialization; + static Future<void>? _pendingInitialization; - ImplicitFlow(this._clientId, this._scopes); + ImplicitFlow(this._clientId, this._scopes, this._enableDebugLogs); /// Readies the flow for calls to [login] by loading the 'gapi' /// JavaScript library, or returning the [Future] of a pending /// initialization if any object has called this method already. Future<void> initialize() { if (_pendingInitialization != null) { - return _pendingInitialization; + return _pendingInitialization!; } - var completer = new Completer(); + final completer = Completer(); - var timeout = new Timer(CallbackTimeout, () { + final timeout = Timer(callbackTimeout, () { _pendingInitialization = null; - completer.completeError(new Exception( - 'Timed out while waiting for the gapi.auth library to load.')); + completer.completeError( + Exception( + 'Timed out while waiting for the gapi.auth library to load.', + ), + ); }); js.context['dartGapiLoaded'] = () { + if (_enableDebugLogs) _gapiAuth2.callMethod('enableDebugLogs', [true]); timeout.cancel(); - try { - var gapi = js.context['gapi']['auth']; - try { - gapi.callMethod('init', [ - () { - completer.complete(); - } - ]); - } on NoSuchMethodError { - throw new StateError('gapi.auth not loaded.'); - } - } catch (error, stack) { - _pendingInitialization = null; - if (!completer.isCompleted) { - completer.completeError(error, stack); - } - } + completer.complete(); }; - var script = _createScript(); - script.src = '${gapiUrl}?onload=dartGapiLoaded'; + final script = _createScript(); + script.src = '$gapiUrl?onload=dartGapiLoaded'; script.onError.first.then((errorEvent) { timeout.cancel(); _pendingInitialization = null; if (!completer.isCompleted) { // script loading errors can still happen after timeouts - completer.completeError(new Exception('Failed to load gapi library.')); + completer.completeError(StateError('Failed to load gapi library.')); } }); - html.document.body.append(script); + html.document.body!.append(script); _pendingInitialization = completer.future; return completer.future; } - Future<LoginResult> loginHybrid( - {bool force: false, bool immediate: false, String loginHint}) => - _login(force, immediate, true, loginHint, null); + Future<LoginResult> loginHybrid({ + String? prompt, + String? loginHint, + String? hostedDomain, + }) => + _login( + prompt: prompt, + responseTypes: [ResponseType.code, ResponseType.token], + loginHint: loginHint, + hostedDomain: hostedDomain, + ); - Future<AccessCredentials> login( - {bool force: false, - bool immediate: false, - String loginHint, - List<ResponseType> responseTypes}) async { - return (await _login(force, immediate, false, loginHint, responseTypes)) - .credential; - } + Future<AccessCredentials> login({ + String? prompt, + String? loginHint, + List<ResponseType>? responseTypes, + String? hostedDomain, + }) async => + (await _login( + prompt: prompt, + loginHint: loginHint, + responseTypes: responseTypes, + hostedDomain: hostedDomain, + )) + .credential; // Completes with either credentials or a tuple of credentials and authCode. // hybrid => [AccessCredentials credentials, String authCode] @@ -125,132 +127,139 @@ // // Alternatively, the response types can be set directly if `hybrid` is not // set to `true`. - Future<LoginResult> _login(bool force, bool immediate, bool hybrid, - String loginHint, List<ResponseType> responseTypes) { - assert(hybrid != true || responseTypes?.isNotEmpty != true); + Future<LoginResult> _login({ + required String? prompt, + required String? hostedDomain, + required String? loginHint, + required List<ResponseType>? responseTypes, + }) { + final completer = Completer<LoginResult>(); - var completer = new Completer<LoginResult>(); - - var gapi = js.context['gapi']['auth']; - - var json = { + // https://developers.google.com/identity/sign-in/web/reference#gapiauth2authorizeconfig + final json = { 'client_id': _clientId, - 'immediate': immediate, - 'approval_prompt': force ? 'force' : 'auto', - 'response_type': responseTypes?.isNotEmpty == true - ? responseTypes - .map((responseType) => _responseTypeToString(responseType)) - .join(' ') - : hybrid - ? 'code token' - : 'token', 'scope': _scopes.join(' '), - 'access_type': hybrid ? 'offline' : 'online', + 'response_type': responseTypes == null || responseTypes.isEmpty + ? 'token' + : responseTypes.map(_responseTypeToString).join(' '), + if (prompt != null) 'prompt': prompt, + // cookie_policy – missing + if (hostedDomain != null) 'hosted_domain': hostedDomain, + if (loginHint != null) 'login_hint': loginHint, + // include_granted_scopes - missing }; - if (loginHint != null) { - json['login_hint'] = loginHint; - } - - gapi.callMethod('authorize', [ - new js.JsObject.jsify(json), - (jsTokenObject) { - var tokenType = jsTokenObject['token_type']; - var token = jsTokenObject['access_token']; - var expiresInRaw = jsTokenObject['expires_in']; - var code = jsTokenObject['code']; - var error = jsTokenObject['error']; - var idToken = jsTokenObject['id_token']; - - var expiresIn; - if (expiresInRaw is String) { - expiresIn = int.parse(expiresInRaw); - } - if (error != null) { - completer.completeError( - new UserConsentException('Failed to get user consent: $error.')); - } else if (token == null || - expiresIn is! int || - tokenType != 'Bearer') { - completer.completeError(new Exception( - 'Failed to obtain user consent. Invalid server response.')); - } else if (responseTypes?.contains(ResponseType.idToken) == true && - idToken?.isNotEmpty != true) { - completer.completeError( - new Exception('Expected to get id_token, but did not.')); - } else { - var accessToken = - new AccessToken('Bearer', token, expiryDate(expiresIn)); - var credentials = new AccessCredentials(accessToken, null, _scopes, - idToken: idToken); - - if (hybrid) { - if (code == null) { - completer.completeError(new Exception('Expected to get auth code ' - 'from server in hybrid flow, but did not.')); - return; - } - completer.complete(new LoginResult(credentials, code: code)); - } else { - completer.complete(new LoginResult(credentials)); - } + _gapiAuth2.callMethod('authorize', [ + js.JsObject.jsify(json), + (js.JsObject jsTokenObject) { + try { + final result = _processToken(jsTokenObject, responseTypes); + completer.complete(result); + } catch (e, stack) { + html.window.console.error(jsTokenObject); + completer.completeError(e, stack); } } ]); return completer.future; } + + LoginResult _processToken( + js.JsObject jsTokenObject, + List<ResponseType>? responseTypes, + ) { + final error = jsTokenObject['error']; + + if (error != null) { + final details = jsTokenObject['details'] as String?; + throw UserConsentException( + 'Failed to get user consent: $error.', + details: details, + ); + } + + final tokenType = jsTokenObject['token_type']; + final token = jsTokenObject['access_token'] as String?; + + if (token == null || tokenType != 'Bearer') { + throw Exception( + 'Failed to obtain user consent. Invalid server response.', + ); + } + + final idToken = jsTokenObject['id_token'] as String?; + + if (responseTypes?.contains(ResponseType.idToken) == true && + idToken?.isNotEmpty != true) { + throw Exception('Expected to get id_token, but did not.'); + } + + List<String>? scopes; + final scopeString = jsTokenObject['scope']; + if (scopeString is String) { + scopes = scopeString.split(' '); + } + + final expiresAt = jsTokenObject['expires_at'] as int; + final expiresAtDate = + DateTime.fromMillisecondsSinceEpoch(expiresAt).toUtc(); + + final accessToken = AccessToken('Bearer', token, expiresAtDate); + final credentials = AccessCredentials( + accessToken, + null, + scopes ?? _scopes, + idToken: idToken, + ); + + String? code; + if (responseTypes?.contains(ResponseType.code) == true) { + code = jsTokenObject['code'] as String?; + + if (code == null) { + throw Exception( + 'Expected to get auth code from server in hybrid flow, but did not.', + ); + } + } + return LoginResult(credentials, code: code); + } } class LoginResult { final AccessCredentials credential; - final String code; + final String? code; LoginResult(this.credential, {this.code}); } /// Convert [responseType] to string value expected by `gapi.auth.authorize`. String _responseTypeToString(ResponseType responseType) { - String result; - switch (responseType) { case ResponseType.code: - result = 'code'; - break; - + return 'code'; case ResponseType.idToken: - result = 'id_token'; - break; - + return 'id_token'; case ResponseType.permission: - result = 'permission'; - break; - + return 'permission'; case ResponseType.token: - result = 'token'; - break; - - default: - throw ArgumentError('Unknown response type: $responseType'); + return 'token'; } - - return result; } /// Creates a script that will run properly when strict CSP is enforced. /// /// More specifically, the script has the correct `nonce` value set. -final _ScriptFactory _createScript = (() { +final html.ScriptElement Function() _createScript = (() { final nonce = _getNonce(); - if (nonce == null) return () => new html.ScriptElement(); + if (nonce == null) return () => html.ScriptElement(); - return () => new html.ScriptElement()..nonce = nonce; + return () => html.ScriptElement()..nonce = nonce; })(); -typedef html.ScriptElement _ScriptFactory(); - /// Returns CSP nonce, if set for any script tag. -String _getNonce({html.Window window}) { +String? _getNonce({html.Window? window}) { final currentWindow = window ?? html.window; final elements = currentWindow.document.querySelectorAll('script'); for (final element in elements) { @@ -262,3 +271,6 @@ } return null; } + +js.JsObject get _gapiAuth2 => + (js.context['gapi'] as js.JsObject)['auth2'] as js.JsObject;
diff --git a/googleapis_auth/lib/src/oauth2_flows/jwt.dart b/googleapis_auth/lib/src/oauth2_flows/jwt.dart index 0aaaf54..39b90f2 100644 --- a/googleapis_auth/lib/src/oauth2_flows/jwt.dart +++ b/googleapis_auth/lib/src/oauth2_flows/jwt.dart
@@ -2,97 +2,72 @@ // 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. -library jwt_token_generator; - import 'dart:async'; import 'dart:convert'; import 'package:http/http.dart' as http; -import '../../auth.dart'; +import '../access_credentials.dart'; import '../crypto/rsa.dart'; import '../crypto/rsa_sign.dart'; -import '../http_client_base.dart'; +import '../known_uris.dart'; import '../utils.dart'; +import 'base_flow.dart'; -class JwtFlow { +class JwtFlow extends BaseFlow { // All details are described at: // https://developers.google.com/accounts/docs/OAuth2ServiceAccount // JSON Web Signature (JWS) requires signing a string with a private key. - static const GOOGLE_OAUTH2_TOKEN_URL = - 'https://accounts.google.com/o/oauth2/token'; - final String _clientEmail; final RS256Signer _signer; final List<String> _scopes; - final String _user; + final String? _user; final http.Client _client; - JwtFlow(this._clientEmail, RSAPrivateKey key, this._user, this._scopes, - this._client) - : _signer = new RS256Signer(key); + JwtFlow( + this._clientEmail, + RSAPrivateKey key, + this._user, + this._scopes, + this._client, + ) : _signer = RS256Signer(key); + @override Future<AccessCredentials> run() async { - int timestamp = new DateTime.now().toUtc().millisecondsSinceEpoch ~/ 1000 - - MAX_EXPECTED_TIMEDIFF_IN_SECONDS; + final timestamp = DateTime.now().toUtc().millisecondsSinceEpoch ~/ 1000 - + maxExpectedTimeDiffInSeconds; - jwtHeader() => {"alg": "RS256", "typ": "JWT"}; + final jwtHeader = {'alg': 'RS256', 'typ': 'JWT'}; + final jwtHeaderBase64 = _base64url(ascii.encode(jsonEncode(jwtHeader))); - jwtClaimSet() { - var claimSet = { - 'iss': _clientEmail, - 'scope': _scopes.join(' '), - 'aud': GOOGLE_OAUTH2_TOKEN_URL, - 'exp': timestamp + 3600, - 'iat': timestamp, - }; - if (_user != null) claimSet['sub'] = _user; - return claimSet; - } + final jwtClaimSet = { + 'iss': _clientEmail, + 'scope': _scopes.join(' '), + 'aud': googleOauth2TokenEndpoint.toString(), + 'exp': timestamp + 3600, + 'iat': timestamp, + if (_user != null) 'sub': _user!, + }; + final jwtClaimSetBase64 = _base64url(utf8.encode(jsonEncode(jwtClaimSet))); - var jwtHeaderBase64 = _base64url(ascii.encode(jsonEncode(jwtHeader()))); - var jwtClaimSetBase64 = _base64url(utf8.encode(jsonEncode(jwtClaimSet()))); - var jwtSignatureInput = '$jwtHeaderBase64.$jwtClaimSetBase64'; - var jwtSignatureInputInBytes = ascii.encode(jwtSignatureInput); + final jwtSignatureInput = '$jwtHeaderBase64.$jwtClaimSetBase64'; + final jwtSignatureInputInBytes = ascii.encode(jwtSignatureInput); - var signature = _signer.sign(jwtSignatureInputInBytes); - var jwt = "$jwtSignatureInput.${_base64url(signature)}"; + final signature = _signer.sign(jwtSignatureInputInBytes); + final jwt = '$jwtSignatureInput.${_base64url(signature)}'; - var uri = 'urn:ietf:params:oauth:grant-type:jwt-bearer'; - var requestParameters = 'grant_type=${Uri.encodeComponent(uri)}&' - 'assertion=${Uri.encodeComponent(jwt)}'; - - var body = new Stream<List<int>>.fromIterable( - <List<int>>[utf8.encode(requestParameters)]); - var request = - new RequestImpl('POST', Uri.parse(GOOGLE_OAUTH2_TOKEN_URL), body); - request.headers['content-type'] = CONTENT_TYPE_URLENCODED; - - var httpResponse = await _client.send(request); - var object = await httpResponse.stream - .transform(utf8.decoder) - .transform(json.decoder) - .first; - Map response = object as Map; - var tokenType = response['token_type']; - var token = response['access_token']; - var expiresIn = response['expires_in']; - var error = response['error']; - - if (httpResponse.statusCode != 200 && error != null) { - throw new Exception('Unable to obtain credentials. Error: $error.'); - } - - if (tokenType != 'Bearer' || token == null || expiresIn is! int) { - throw new Exception( - 'Unable to obtain credentials. Invalid response from server.'); - } - var accessToken = new AccessToken(tokenType, token, expiryDate(expiresIn)); - return new AccessCredentials(accessToken, null, _scopes); - } - - String _base64url(List<int> bytes) { - return base64Url.encode(bytes).replaceAll('=', ''); + // https://developers.google.com/identity/protocols/oauth2/service-account#authorizingrequests + final response = await _client.oauthTokenRequest({ + 'grant_type': _uri, + 'assertion': jwt, + }); + final accessToken = parseAccessToken(response); + return AccessCredentials(accessToken, null, _scopes); } } + +const _uri = 'urn:ietf:params:oauth:grant-type:jwt-bearer'; + +String _base64url(List<int> bytes) => + base64Url.encode(bytes).replaceAll('=', '');
diff --git a/googleapis_auth/lib/src/oauth2_flows/metadata_server.dart b/googleapis_auth/lib/src/oauth2_flows/metadata_server.dart index 5ca013f..26bf224 100644 --- a/googleapis_auth/lib/src/oauth2_flows/metadata_server.dart +++ b/googleapis_auth/lib/src/oauth2_flows/metadata_server.dart
@@ -2,16 +2,14 @@ // 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. -library googleapis_auth.metadata_server_flow; - import 'dart:async'; -import 'dart:convert'; import 'dart:io'; import 'package:http/http.dart' as http; -import '../../auth.dart'; +import '../access_credentials.dart'; import '../utils.dart'; +import 'base_flow.dart'; /// Obtains access credentials form the metadata server. /// @@ -19,75 +17,76 @@ /// ComputeEngine VM. It will retrieve the current access token from the /// metadata server, looking first for one set in the environment under /// `$GCE_METADATA_HOST`. -class MetadataServerAuthorizationFlow { - static const _HEADERS = const {'Metadata-Flavor': 'Google'}; - static const _SERVICE_ACCOUNT_URL_INFIX = +class MetadataServerAuthorizationFlow extends BaseFlow { + static const _headers = {'Metadata-Flavor': 'Google'}; + static const _serviceAccountUrlInfix = 'computeMetadata/v1/instance/service-accounts'; - static const _DEFAULT_METADATA_HOST = "metadata"; - static const _GCE_METADATA_HOST_ENV_VAR = "GCE_METADATA_HOST"; + // https://cloud.google.com/compute/docs/storing-retrieving-metadata#querying + static const _defaultMetadataHost = 'metadata.google.internal'; + static const _gceMetadataHostEnvVar = 'GCE_METADATA_HOST'; final String email; final Uri _scopesUrl; final Uri _tokenUrl; final http.Client _client; - factory MetadataServerAuthorizationFlow(http.Client client, - {String email: 'default'}) { - var encodedEmail = Uri.encodeComponent(email); + factory MetadataServerAuthorizationFlow( + http.Client client, { + String email = 'default', + }) { + final encodedEmail = Uri.encodeComponent(email); - final metadataHost = Platform.environment[_GCE_METADATA_HOST_ENV_VAR] ?? - _DEFAULT_METADATA_HOST; + final metadataHost = + Platform.environment[_gceMetadataHostEnvVar] ?? _defaultMetadataHost; final serviceAccountPrefix = - "http://$metadataHost/$_SERVICE_ACCOUNT_URL_INFIX"; + 'http://$metadataHost/$_serviceAccountUrlInfix'; - var scopesUrl = Uri.parse('$serviceAccountPrefix/$encodedEmail/scopes'); - var tokenUrl = Uri.parse('$serviceAccountPrefix/$encodedEmail/token'); - return new MetadataServerAuthorizationFlow._( - client, email, scopesUrl, tokenUrl); + final scopesUrl = Uri.parse('$serviceAccountPrefix/$encodedEmail/scopes'); + final tokenUrl = Uri.parse('$serviceAccountPrefix/$encodedEmail/token'); + return MetadataServerAuthorizationFlow._( + client, + email, + scopesUrl, + tokenUrl, + ); } MetadataServerAuthorizationFlow._( - this._client, this.email, this._scopesUrl, this._tokenUrl); + this._client, + this.email, + this._scopesUrl, + this._tokenUrl, + ); + @override Future<AccessCredentials> run() async { - final results = await Future.wait([_getToken(), _getScopes()]); - final Map token = results.first; - final String scopesString = results.last; + final results = await Future.wait( + [ + _client.requestJson( + http.Request('GET', _tokenUrl)..headers.addAll(_headers), + 'Failed to obtain access credentials.', + ), + _getScopes() + ], + ); + final json = results.first as Map<String, dynamic>; + final accessToken = parseAccessToken(json); - var json = token; - var scopes = scopesString + final scopes = (results.last as String) .replaceAll('\n', ' ') .split(' ') - .where((part) => part.length > 0) + .where((part) => part.isNotEmpty) .toList(); - var type = json['token_type']; - var accessToken = json['access_token']; - var expiresIn = json['expires_in']; - var error = json['error']; - - if (error != null) { - throw new Exception('Error while obtaining credentials from metadata ' - 'server. Error message: $error.'); - } - - if (type != 'Bearer' || accessToken == null || expiresIn is! int) { - throw new Exception('Invalid response from metadata server.'); - } - - return new AccessCredentials( - new AccessToken(type, accessToken, expiryDate(expiresIn)), - null, - scopes); - } - - Future<Map> _getToken() async { - var response = await _client.get(_tokenUrl, headers: _HEADERS); - return jsonDecode(response.body); + return AccessCredentials( + accessToken, + null, + scopes, + ); } Future<String> _getScopes() async { - var response = await _client.get(_scopesUrl, headers: _HEADERS); + final response = await _client.get(_scopesUrl, headers: _headers); return response.body; } }
diff --git a/googleapis_auth/lib/src/response_type.dart b/googleapis_auth/lib/src/response_type.dart new file mode 100644 index 0000000..daebe33 --- /dev/null +++ b/googleapis_auth/lib/src/response_type.dart
@@ -0,0 +1,23 @@ +// Copyright (c) 2021, 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. + +/// Available response types that can be requested when using the implicit +/// browser login flow. +/// +/// More information about these values can be found here: +/// https://developers.google.com/identity/protocols/oauth2/openid-connect#response-type +enum ResponseType { + /// Requests an access code. This triggers the basic rather than the implicit + /// flow. + code, + + /// Requests the user's identity token when running the implicit flow. + idToken, + + /// Requests the user's current permissions. + permission, + + /// Requests the user's access token when running the implicit flow. + token, +}
diff --git a/googleapis_auth/lib/src/service_account_client.dart b/googleapis_auth/lib/src/service_account_client.dart new file mode 100644 index 0000000..ecc5e63 --- /dev/null +++ b/googleapis_auth/lib/src/service_account_client.dart
@@ -0,0 +1,57 @@ +// Copyright (c) 2021, 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'; + +import 'oauth2_flows/base_flow.dart'; +import 'oauth2_flows/jwt.dart'; +import 'service_account_credentials.dart'; + +/// Obtain oauth2 [AccessCredentials] using service account credentials. +/// +/// In case the service account has no access to the requested scopes or another +/// error occurs the returned future will complete with an `Exception`. +/// +/// {@macro googleapis_auth_client_for_creds} +/// +/// The [ServiceAccountCredentials] can be obtained in the Google Cloud Console. +Future<AccessCredentials> obtainAccessCredentialsViaServiceAccount( + ServiceAccountCredentials clientCredentials, + List<String> scopes, + Client client, +) => + JwtFlow( + clientCredentials.email, + clientCredentials.privateRSAKey, + clientCredentials.impersonatedUser, + scopes, + client, + ).run(); + +/// Obtains oauth2 credentials and returns an authenticated HTTP client. +/// +/// See [obtainAccessCredentialsViaServiceAccount] for specifics about the +/// arguments used for obtaining access credentials. +/// +/// {@macro googleapis_auth_returned_auto_refresh_client} +/// +/// {@macro googleapis_auth_baseClient_param} +/// +/// {@macro googleapis_auth_close_the_client} +/// {@macro googleapis_auth_not_close_the_baseClient} +Future<AutoRefreshingAuthClient> clientViaServiceAccount( + ServiceAccountCredentials clientCredentials, + List<String> scopes, { + Client? baseClient, +}) async => + await clientFromFlow( + (c) => JwtFlow( + clientCredentials.email, + clientCredentials.privateRSAKey, + clientCredentials.impersonatedUser, + scopes, + c, + ), + baseClient: baseClient, + );
diff --git a/googleapis_auth/lib/src/service_account_credentials.dart b/googleapis_auth/lib/src/service_account_credentials.dart new file mode 100644 index 0000000..0646838 --- /dev/null +++ b/googleapis_auth/lib/src/service_account_credentials.dart
@@ -0,0 +1,94 @@ +// Copyright (c) 2014, 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 'client_id.dart'; +import 'crypto/pem.dart'; +import 'crypto/rsa.dart'; + +export 'access_credentials.dart' show AccessCredentials; +export 'access_token.dart' show AccessToken; +export 'auth_client.dart'; +export 'client_id.dart'; +export 'exceptions.dart'; +export 'response_type.dart'; + +/// Represents credentials for a service account. +class ServiceAccountCredentials { + /// The email address of this service account. + final String email; + + /// The clientId. + final ClientId clientId; + + /// Private key. + final String privateKey; + + /// Impersonated user, if any. If not impersonating any user this is `null`. + final String? impersonatedUser; + + /// Private key as an [RSAPrivateKey]. + final RSAPrivateKey privateRSAKey; + + /// Creates a new [ServiceAccountCredentials] from JSON. + /// + /// [json] can be either a [Map] or a JSON map encoded as a [String]. + /// + /// The optional named argument [impersonatedUser] is used to set the user + /// to impersonate if impersonating a user. + factory ServiceAccountCredentials.fromJson(json, {String? impersonatedUser}) { + if (json is String) { + json = jsonDecode(json); + } + if (json is! Map) { + throw ArgumentError('json must be a Map or a String encoding a Map.'); + } + final identifier = json['client_id'] as String?; + final privateKey = json['private_key'] as String?; + final email = json['client_email'] as String?; + final type = json['type']; + + if (type != 'service_account') { + throw ArgumentError( + 'The given credentials are not of type ' + 'service_account (was: $type).', + ); + } + + if (identifier == null || privateKey == null || email == null) { + throw ArgumentError( + 'The given credentials do not contain all the ' + 'fields: client_id, private_key and client_email.', + ); + } + + final clientId = ClientId(identifier); + return ServiceAccountCredentials( + email, + clientId, + privateKey, + impersonatedUser: impersonatedUser, + ); + } + + /// Creates a new [ServiceAccountCredentials]. + /// + /// [email] is the e-mail address of the service account. + /// + /// [clientId] is the client ID for the service account. + /// + /// [privateKey] is the base 64 encoded, unencrypted private key, including + /// the '-----BEGIN PRIVATE KEY-----' and '-----END PRIVATE KEY-----' + /// boundaries. + /// + /// The optional named argument [impersonatedUser] is used to set the user + /// to impersonate if impersonating a user is needed. + ServiceAccountCredentials( + this.email, + this.clientId, + this.privateKey, { + this.impersonatedUser, + }) : privateRSAKey = keyFromString(privateKey); +}
diff --git a/googleapis_auth/lib/src/typedefs.dart b/googleapis_auth/lib/src/typedefs.dart index d169ad6..0010862 100644 --- a/googleapis_auth/lib/src/typedefs.dart +++ b/googleapis_auth/lib/src/typedefs.dart
@@ -2,13 +2,11 @@ // 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. -library googleapis_auth.typedefs; - /// Function for directing the user or it's user-agent to [uri]. /// /// The user is required to go to [uri] and either approve or decline the /// application's request for access resources on his behalf. -typedef void PromptUserForConsent(String uri); +typedef PromptUserForConsent = void Function(String uri); /// Function for directing the user or it's user-agent to [uri]. /// @@ -18,4 +16,4 @@ /// The user will be given an authorization code. This function should complete /// with this authorization code. If the user declined to give access this /// function should complete with an error. -typedef Future<String> PromptUserForConsentManual(String uri); +typedef PromptUserForConsentManual = Future<String> Function(String uri);
diff --git a/googleapis_auth/lib/src/utils.dart b/googleapis_auth/lib/src/utils.dart index 8aed0ff..785473b 100644 --- a/googleapis_auth/lib/src/utils.dart +++ b/googleapis_auth/lib/src/utils.dart
@@ -2,20 +2,171 @@ // 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. -library googleapis_auth.utils; +import 'dart:convert'; + +import 'package:http/http.dart' show BaseRequest, Client, StreamedResponse; +import 'package:http_parser/http_parser.dart'; + +import 'access_token.dart'; +import 'exceptions.dart'; +import 'http_client_base.dart'; +import 'known_uris.dart'; /// Due to differences of clock speed, network latency, etc. we /// will shorten expiry dates by 20 seconds. -const MAX_EXPECTED_TIMEDIFF_IN_SECONDS = 20; +const maxExpectedTimeDiffInSeconds = 20; -/// Constructs a [DateTime] which is [seconds] seconds from now with -/// an offset of [MAX_EXPECTED_TIMEDIFF_IN_SECONDS]. Result is UTC time. -DateTime expiryDate(int seconds) { - return new DateTime.now() - .toUtc() - .add(new Duration(seconds: seconds - MAX_EXPECTED_TIMEDIFF_IN_SECONDS)); +AccessToken parseAccessToken(Map<String, dynamic> jsonMap) { + final tokenType = jsonMap['token_type']; + final accessToken = jsonMap['access_token']; + final expiresIn = jsonMap['expires_in']; + + if (accessToken is! String || expiresIn is! int || tokenType != 'Bearer') { + throw ServerRequestFailedException( + 'Failed to exchange authorization code. Invalid server response.', + responseContent: jsonMap, + ); + } + + return AccessToken('Bearer', accessToken, expiryDate(expiresIn)); } +/// Constructs a [DateTime] which is [seconds] seconds from now with +/// an offset of [maxExpectedTimeDiffInSeconds]. Result is UTC time. +DateTime expiryDate(int seconds) => DateTime.now() + .toUtc() + .add(Duration(seconds: seconds - maxExpectedTimeDiffInSeconds)); + /// Constant for the 'application/x-www-form-urlencoded' content type -const CONTENT_TYPE_URLENCODED = +const _contentTypeUrlEncoded = 'application/x-www-form-urlencoded; charset=utf-8'; + +Future<Map<String, dynamic>> _readJsonMapFromResponse( + StreamedResponse response, +) async { + await _expectJsonResponse(response); + + Object? jsonValue; + + final bytes = await response.stream.toBytes(); + + late String string; + try { + string = utf8.decode(bytes); + } on FormatException catch (e) { + throw ServerRequestFailedException( + 'The response was not valid UTF-8. ' + '$e', + statusCode: response.statusCode, + responseContent: bytes, + ); + } + + try { + jsonValue = jsonDecode(string); + } on FormatException catch (e) { + throw ServerRequestFailedException( + 'Could not decode the response as JSON. ' + '$e', + statusCode: response.statusCode, + responseContent: string, + ); + } + + if (jsonValue is! Map<String, dynamic>) { + throw ServerRequestFailedException( + 'The returned JSON response was not a Map.', + statusCode: response.statusCode, + responseContent: jsonValue, + ); + } + + return jsonValue; +} + +extension ClientExtensions on Client { + Future<Map<String, dynamic>> requestJson( + BaseRequest request, + String errorHeader, + ) async { + final response = await send(request); + final jsonMap = await _readJsonMapFromResponse(response); + + if (response.statusCode != 200) { + final error = _errorStringFromJsonResponse(jsonMap); + final message = [ + errorHeader, + if (error != null) error, + ].join(' '); + throw ServerRequestFailedException( + message, + statusCode: response.statusCode, + responseContent: jsonMap, + ); + } + + return jsonMap; + } + + Future<Map<String, dynamic>> oauthTokenRequest( + Map<String, String> postValues, + ) async { + final body = Stream<List<int>>.value( + ascii.encode( + postValues.entries + .map((e) => '${e.key}=${Uri.encodeComponent(e.value)}') + .join('&'), + ), + ); + final request = RequestImpl('POST', googleOauth2TokenEndpoint, body) + ..headers['content-type'] = _contentTypeUrlEncoded; + + return requestJson(request, 'Failed to obtain access credentials.'); + } +} + +/// Returns an error string for [json] if it contains error data in keys +/// `error` and `error_description`. +/// +/// Otherwise, returns `null`. +String? _errorStringFromJsonResponse(Map<String, dynamic> json) { + final error = json['error']; + final values = [ + if (error != null) 'Error: $error', + json['error_description'], + ].where((element) => element != null).join(' '); + if (values.isEmpty) return null; + return values; +} + +Future<void> _expectJsonResponse(StreamedResponse response) async { + final contentType = response.headers['content-type']; + + if (!_isJson(contentType)) { + String? body; + try { + body = await response.stream.bytesToString(); + } catch (_) { + /// We're already going to throw below + } + + final message = contentType == null + ? 'Server responded without a content type header.' + : 'Server responded with invalid content type: $contentType. '; + + throw ServerRequestFailedException( + '$message Expected a JSON response.', + statusCode: response.statusCode, + responseContent: body, + ); + } +} + +/// Follows https://mimesniff.spec.whatwg.org/#json-mime-type +bool _isJson(String? contentType) { + if (contentType == null) return false; + final mediaType = MediaType.parse(contentType); + if (mediaType.mimeType == 'application/json') return true; + if (mediaType.mimeType == 'text/json') return true; + return mediaType.subtype.endsWith('+json'); +}
diff --git a/googleapis_auth/mono_pkg.yaml b/googleapis_auth/mono_pkg.yaml new file mode 100644 index 0000000..39fbee2 --- /dev/null +++ b/googleapis_auth/mono_pkg.yaml
@@ -0,0 +1,13 @@ +# See https://pub.dev/packages/mono_repo +dart: +- 2.13.0 +- dev + +stages: +- analyze_and_format: + - group: + - format + - analyze: --fatal-infos . +- unittest: + - test: -p vm + - test: -p chrome
diff --git a/googleapis_auth/pubspec.yaml b/googleapis_auth/pubspec.yaml index 50310c6..7278c3b 100644 --- a/googleapis_auth/pubspec.yaml +++ b/googleapis_auth/pubspec.yaml
@@ -1,13 +1,22 @@ name: googleapis_auth -version: 0.2.12+1 +version: 1.3.0 description: Obtain Access credentials for Google services using OAuth 2.0 -homepage: https://github.com/dart-lang/googleapis_auth +repository: https://github.com/dart-lang/googleapis environment: - sdk: '>=2.1.0 <3.0.0' + sdk: '>=2.13.0 <3.0.0' dependencies: - crypto: '>=0.9.2 <4.0.0' - http: '>=0.11.3+17 <0.13.0' + crypto: ^3.0.0 + http: ^0.13.0 + http_parser: ^4.0.0 dev_dependencies: - test: ^1.3.0 + # build_ dependencies allow debugging web tests using: + # `dart pub run build_runner serve test` + build_runner: ^2.0.0 + build_test: ^2.0.0 + build_web_compilers: ^3.0.0 + test: ^1.16.0 + +false_secrets: +- test/test_utils.dart
diff --git a/grpc/AUTHORS b/grpc/AUTHORS new file mode 100644 index 0000000..7a6111c --- /dev/null +++ b/grpc/AUTHORS
@@ -0,0 +1,8 @@ +# Below is a list of people and organizations that have contributed +# to the project. Names should be added to the list like so: +# +# Name/Organization <email address> + +Google Inc. +German Saprykin <saprykin.h@gmail.com> +Alexandre Ardhuin <alexandre.ardhuin@gmail.com>
diff --git a/grpc/BUILD.gn b/grpc/BUILD.gn new file mode 100644 index 0000000..12e6301 --- /dev/null +++ b/grpc/BUILD.gn
@@ -0,0 +1,87 @@ +# This file is generated by package_importer.py for grpc-3.0.2 + +import("//build/dart/dart_library.gni") + +dart_library("grpc") { + package_name = "grpc" + + language_version = "2.12" + + disable_analysis = true + + deps = [ + "//third_party/dart-pkg/pub/archive", + "//third_party/dart-pkg/pub/async", + "//third_party/dart-pkg/pub/crypto", + "//third_party/dart-pkg/pub/fixnum", + "//third_party/dart-pkg/pub/googleapis_auth", + "//third_party/dart-pkg/pub/meta", + "//third_party/dart-pkg/pub/http", + "//third_party/dart-pkg/pub/http2", + "//third_party/dart-pkg/pub/protobuf", + ] + + sources = [ + "grpc.dart", + "grpc_connection_interface.dart", + "grpc_or_grpcweb.dart", + "grpc_web.dart", + "service_api.dart", + "src/auth/auth.dart", + "src/auth/auth_io.dart", + "src/auth/rsa.dart", + "src/client/call.dart", + "src/client/channel.dart", + "src/client/client.dart", + "src/client/client_transport_connector.dart", + "src/client/common.dart", + "src/client/connection.dart", + "src/client/grpc_or_grpcweb_channel_grpc.dart", + "src/client/grpc_or_grpcweb_channel_web.dart", + "src/client/http2_channel.dart", + "src/client/http2_connection.dart", + "src/client/interceptor.dart", + "src/client/method.dart", + "src/client/options.dart", + "src/client/query_parameter.dart", + "src/client/transport/cors.dart", + "src/client/transport/http2_credentials.dart", + "src/client/transport/http2_transport.dart", + "src/client/transport/transport.dart", + "src/client/transport/web_streams.dart", + "src/client/transport/xhr_transport.dart", + "src/client/web_channel.dart", + "src/generated/google/protobuf/any.pb.dart", + "src/generated/google/protobuf/any.pbenum.dart", + "src/generated/google/protobuf/any.pbjson.dart", + "src/generated/google/protobuf/duration.pb.dart", + "src/generated/google/protobuf/duration.pbenum.dart", + "src/generated/google/protobuf/duration.pbjson.dart", + "src/generated/google/rpc/code.pb.dart", + "src/generated/google/rpc/code.pbenum.dart", + "src/generated/google/rpc/code.pbjson.dart", + "src/generated/google/rpc/error_details.pb.dart", + "src/generated/google/rpc/error_details.pbenum.dart", + "src/generated/google/rpc/error_details.pbjson.dart", + "src/generated/google/rpc/status.pb.dart", + "src/generated/google/rpc/status.pbenum.dart", + "src/generated/google/rpc/status.pbjson.dart", + "src/server/call.dart", + "src/server/handler.dart", + "src/server/interceptor.dart", + "src/server/server.dart", + "src/server/service.dart", + "src/shared/api.dart", + "src/shared/codec.dart", + "src/shared/codec_registry.dart", + "src/shared/io_bits/io_bits.dart", + "src/shared/io_bits/io_bits_io.dart", + "src/shared/io_bits/io_bits_web.dart", + "src/shared/message.dart", + "src/shared/profiler.dart", + "src/shared/security.dart", + "src/shared/status.dart", + "src/shared/streams.dart", + "src/shared/timeout.dart", + ] +}
diff --git a/grpc/CHANGELOG.md b/grpc/CHANGELOG.md new file mode 100644 index 0000000..f9a4187 --- /dev/null +++ b/grpc/CHANGELOG.md
@@ -0,0 +1,270 @@ +## 3.0.2 + +* Fix compilation on the Web with DDC. + +## 3.0.1 + +* Require `package:googleapis_auth` `^1.1.0` +* Fix issues [#421](https://github.com/grpc/grpc-dart/issues/421) and + [#458](https://github.com/grpc/grpc-dart/issues/458). Validate + responses according to gRPC/gRPC-Web protocol specifications: require + 200 HTTP status and a supported `Content-Type` header to be present, as well + as `grpc-status: 0` header. When handling malformed responses make effort + to translate HTTP statuses into gRPC statuses. +* Add GrpcOrGrpcWebClientChannel which uses gRPC on all platforms except web, + on which it uses gRPC-web. +* `GrpcError` now exposes response trailers via `GrpcError.trailers`. + +## 3.0.0 + +* Migrate library and tests to null safety. +* Require Dart 2.12 or greater. + +## 2.9.0 + +* Added support for compression/decompression, which can be configured through + `ChannelOptions` constructor's `codecRegistry` parameter or adding the + `grpc-accept-encoding` to `metadata` parameter of `CallOptions` on the client + side and `codecRegistry` parameter to `Server` on the server side. + Outgoing rpc can be compressed using the `compression` parameter on the + `CallOptions`. +* Fix issue [#206](https://github.com/grpc/grpc-dart/issues/206). Prevent an + exception to be thrown when a web connection stream is closed. +* Add XHR raw response to the GrpcError for a better debugging + ([PR #423](https://github.com/grpc/grpc-dart/pulls/423)). + +Note: this is the last release supporting SDK < 2.12. Next release will +be nullsafe and thus require SDK >= 2.12. + +## 2.8.0 + +* Added support for client interceptors, which can be configured through + `Client` constructor's `interceptors` parameter. Interceptors will be + executed by `Client.$createStreamingCall` and `Client.$createUnaryCall`. + Using interceptors requires regenerating client stubs using version 19.2.0 or + newer of protobuf compiler plugin. +* `Client.$createCall` is deprecated because it does not invoke client + interceptors. +* Fix issue [#380](https://github.com/grpc/grpc-dart/issues/380) causing + incorrect duplicated headers in gRPC-Web requests. +* Change minimum required Dart SDK to 2.8 to enable access to Unix domain sockets. +* Add support for Unix domain sockets in `Socket.serve` and `ClientChannel`. +* Fix issue [#331](https://github.com/grpc/grpc-dart/issues/331) causing + an exception in `GrpcWebClientChannel.terminate()`. + +## 2.7.0 + +* Added decoding/parsing of `grpc-status-details-bin` to pass all response + exception details to the `GrpcError` thrown in Dart, via + [#349](https://github.com/grpc/grpc-dart/pull/349). +* Dart SDK constraint is bumped to `>=2.3.0 <3.0.0` due to language version + in the generated protobuf code. + +## 2.6.0 + +* Create gRPC servers and clients with [Server|Client]TransportConnection. + This allows callers to provide their own transport configuration, such + as their own implementation of streams and sinks instead of sockets. + +## 2.5.0 + +* Expose a `validateClient` method for server credentials so gRPC server + users may know when clients are loopback addresses. + +## 2.4.1 + +* Plumb stacktraces through request / response stream error handlers. +* Catch and forward any errors decoding the response. + +## 2.4.0 + +* Add the ability to bypass CORS preflight requests. + +## 2.3.0 + +* Revert [PR #287](https://github.com/grpc/grpc-dart/pull/287), which allowed +using gRPC-web in native environments but also broke streaming. + +## 2.2.0+1 + +* Relax `crypto` version dependency constraint from `^2.1.5` to `^2.1.4`. + +## 2.2.0 + +* Added `applicationDefaultCredentialsAuthenticator` function for creating an + authenticator using [Application Default Credentials](https://cloud.google.com/docs/authentication/production). +* Less latency by using the `tcpNoDelay` option for sockets. +* Support grpc-web in a non-web setting. + +## 2.1.3 + +* Fix bug in grpc-web when receiving an empty trailer. +* Fix a state bug in the server. + +## 2.1.2 + +* Fix bug introduced in 2.1.1 where the port would be added to the default authority when making a + secure connection. + +## 2.1.1 + +* Fix bug introduced in 2.1.0 where an explicit `authority` would not be used when making a secure + connection. + +## 2.1.0 + +* Do a health check of the http2-connection before making request. +* Introduce `ChannelOptions.connectionLimit` the longest time a single connection is used for new + requests. +* Use Tcp.nodelay to improve client call speed. +* Use SecureSocket supportedProtocols to save a round trip when establishing a secure connection. +* Allow passing http2 `ServerSettings` to `Server.serve`. + +## 2.0.3 + +* GrpcError now implements Exception to indicate it can be reasonably handled. + +## 2.0.2 + +* Fix computation of the audience given to metadata providers to include the scheme. + +## 2.0.1 + +* Fix computation of authority. This should fix authorization. + +## 2.0.0+1 + +* Fix imports to ensure `grpc_web.dart` has no accidental transitive dependencies on dart:io. + +## 2.0.0 + +* Add initial support for grpc-web. + See `example/grpc-web` for an example of this working. +* **Breaking**: `grpc.dart` no longer exposes `ClientConnection`. It was supposed to be an internal + abstraction. +* **Breaking**: `grpc.dart` no longer exposes the deprecated `ServerHandler`. + It was supposed to be an internal abstraction. +* `service_api.dart` no longer exports Server - it has never been used by the generated code. + +## 1.0.3 + +* Allow custom user agent with a `userAgent` argument for `ChannelOptions()`. +* Allow specifying `authority` for `ChannelCredentials.insecure()`. +* Add `userAgent` as an optional named argument for `clientConnection.createCallHeaders()`. + +## 1.0.2 + +* Fix bug where the server would crash if the client would break the connection. + +## 1.0.1 + +* Add `service_api.dart` that only contains the minimal imports needed by the code generated by + protoc_plugin. + +## 1.0.0+1 + +* Support package:http2 1.0.0. + +## 1.0.0 + +* Graduate package to 1.0. + +## 0.6.8+1 + +* Removes stray files that where published by accident in version 0.6.8. + +## 0.6.8 + +* Calling `terminate()` or `shutdown()` on a channel doesn't throw error if the +channel is not yet open. + +## 0.6.7 + +* Support package:test 1.5. + +## 0.6.6 + +* Support `package:http` `>=0.11.3+17 <0.13.0`. +* Update `package:googleapis_auth` to `^0.2.5+3`. + +## 0.6.5 + +* Interceptors are now async. + +## 0.6.4 + +* Update dependencies to be compatible with Dart 2. + +## 0.6.3 + +* Make fields of `StatusCode` const rather than final. + +## 0.6.2 + +* Allow for non-ascii header values. + +## 0.6.1 + +* More fixes to update to Dart 2 core library APIs. + +## 0.6.0+1 + +* Updated implementation to use new Dart 2 APIs using +[dart2_fix](https://github.com/dart-lang/dart2_fix). + +## 0.6.0 + +* Dart SDK upper constraint raised to declare compatibility with Dart 2.0 stable. + +## 0.5.0 + +* Breaking change: The package now exclusively supports Dart 2. +* Fixed tests to pass in Dart 2. +* Added support for Interceptors ([issue #79](https://github.com/grpc/grpc-dart/issues/79)); thanks to [@mogol](https://github.com/mogol) for contributing! + +## 0.4.1 + +* Fixes for supporting Dart 2. + +## 0.4.0 + +* Moved TLS credentials for server into a separate class. +* Added support for specifying the address for the server, and support for + serving on an ephemeral port. + +## 0.3.1 + +* Split out TLS credentials to a separate class. + +## 0.3.0 + +* Added authentication metadata providers, optimized for use with Google Cloud. +* Added service URI to metadata provider API, needed for Json Web Token generation. +* Added authenticated cloud-to-prod interoperability tests. +* Refactored connection logic to throw initial connection errors early. + +## 0.2.1 + +* Updated generated code in examples using latest protoc compiler plugin. +* Dart 2.0 fixes. +* Changed license to Apache 2.0. + +## 0.2.0 + +* Implemented support for per-RPC metadata providers. This can be used for + authentication providers which may need to obtain or refresh a token before + the RPC is sent. + +## 0.1.0 + +* Core gRPC functionality is implemented and passes +[gRPC compliance tests](https://github.com/grpc/grpc/blob/master/doc/interop-test-descriptions.md). + +The API is shaping up, but may still change as more advanced features are implemented. + +## 0.0.1 + +* Initial version. + +This package is in a very early and experimental state. We do not recommend +using it for anything but experiments.
diff --git a/grpc/CODE-OF-CONDUCT.md b/grpc/CODE-OF-CONDUCT.md new file mode 100644 index 0000000..9d4213e --- /dev/null +++ b/grpc/CODE-OF-CONDUCT.md
@@ -0,0 +1,3 @@ +## Community Code of Conduct + +gRPC follows the [CNCF Code of Conduct](https://github.com/cncf/foundation/blob/master/code-of-conduct.md).
diff --git a/grpc/CONTRIBUTING.md b/grpc/CONTRIBUTING.md new file mode 100644 index 0000000..16fc729 --- /dev/null +++ b/grpc/CONTRIBUTING.md
@@ -0,0 +1,72 @@ +# How to contribute + +We definitely welcome your patches and contributions to gRPC! + +If you are new to github, please start by reading [Pull Request +howto](https://help.github.com/articles/about-pull-requests/) + +## Legal requirements + +In order to protect both you and ourselves, you will need to sign the +[Contributor License +Agreement](https://identity.linuxfoundation.org/projects/cncf). + +## Code style + +We follow the [Effective +Dart](https://www.dartlang.org/guides/language/effective-dart/style) code style. +We rely on auto-formatting all whitespace -- this avoids having to discuss +formatting during reviews. To format your code, run `dartfmt` from the root, or +use the similar action in your [favorite Dart +editor](https://www.dartlang.org/tools). + +All code must pass Dart analysis. If you are using an IDE or Dart-enabled editor +it should raise analysis issues as you edit; alternatively validate from the +Terminal: + +``` +dartanalyzer lib test +``` + +All analysis warnings and errors must be fixed; hints should be considered. + +## Running tests + +``` +pub get +pub run test +``` + +gRPC-web tests require [`envoy`]( +https://www.envoyproxy.io/docs/envoy/latest/start/start.html) binary to be +available in the PATH. + +## Guidelines for Pull Requests + +How to get your contributions merged smoothly and quickly. + +- Create **small PRs** that are narrowly focused on **addressing a single +concern**. + +- For speculative changes, consider opening an issue and discussing it first. + +- Provide a good **PR description** as a record of **what** change is being made +and **why** it was made. Link to a github issue if it exists. + +- Unless your PR is trivial, you should expect there will be review comments +that you'll need to address before merging. We expect you to be reasonably +responsive to those comments, otherwise the PR will be closed after 2-3 weeks of +inactivity. + +- Keep your PR up to date with upstream/master (if there are merge conflicts, we +can't really merge your change). + +- **All tests need to be passing** before your change can be merged. We +recommend you **run tests locally** before creating your PR to catch breakages +early on. + +- Exceptions to the rules can be made if there's a compelling reason for doing +so. + +## Updating protobuf definitions +Sometimes we might need to update the generated dart files from the protos included in `lib/src/protos`. To do this, run the script `tool/regenerate.sh` from the project root and it will update the generated dart files in `lib/src/geneerated`. \ No newline at end of file
diff --git a/grpc/LICENSE b/grpc/LICENSE new file mode 100644 index 0000000..c11903c --- /dev/null +++ b/grpc/LICENSE
@@ -0,0 +1,176 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS
diff --git a/grpc/MAINTAINERS.md b/grpc/MAINTAINERS.md new file mode 100644 index 0000000..44bd18c --- /dev/null +++ b/grpc/MAINTAINERS.md
@@ -0,0 +1,15 @@ +This page lists all active maintainers of this repository. If you were a +maintainer and would like to add your name to the Emeritus list, please send us a +PR. + +See [CONTRIBUTING.md](https://github.com/grpc/grpc-community/blob/master/CONTRIBUTING.md) +for general contribution guidelines. + +## Maintainers (in alphabetical order) +- [mit-mit](https://github.com/mit-mit), Google LLC +- [nichite](https://github.com/nichite), Google LLC +- [szakarias](https://github.com/szakarias), Google LLC +- [sigurdm](https://github.com/sigurdm), Google LLC + +## Emeritus Maintainers (in alphabetical order) +- [jakobr-google](https://github.com/jakobr-google), Google LLC
diff --git a/grpc/README.md b/grpc/README.md new file mode 100644 index 0000000..667f28d --- /dev/null +++ b/grpc/README.md
@@ -0,0 +1,28 @@ +The [Dart](https://www.dart.dev/) implementation of +[gRPC](https://grpc.io/): A high performance, open source, general RPC framework that puts mobile and HTTP/2 first. + +[](https://github.com/grpc/grpc-dart/actions?query=workflow%3A%22Dart%22+branch%3Amaster) +[](https://pub.dev/packages/grpc) + + +## Learn more + +- [Quick Start](https://grpc.io/docs/languages/dart/quickstart) - get an app running in minutes +- [Examples](example) +- [API reference](https://grpc.io/docs/languages/dart/api) + +For complete documentation, see [Dart gRPC](https://grpc.io/docs/languages/dart). + +## Supported platforms + +- [Dart native](https://dart.dev/platforms) +- [Flutter](https://flutter.dev) + +> **Note:** [grpc-web](https://github.com/grpc/grpc-web) is supported by `package:grpc/grpc_web.dart`. +> **UDS-unix domain socket** is supported with sdk version >= 2.8.0. + +## Contributing + +If you experience problems or have feature requests, [open an issue](https://github.com/dart-lang/grpc-dart/issues/new). + +Note that we have limited bandwidth to accept PRs, and that all PRs require signing the [EasyCLA](https://lfcla.com).
diff --git a/grpc/SECURITY.md b/grpc/SECURITY.md new file mode 100644 index 0000000..234a83c --- /dev/null +++ b/grpc/SECURITY.md
@@ -0,0 +1,3 @@ +# Security Policy + +For information on the gRPC-Dart security policy, please see https://dart.dev/security.
diff --git a/grpc/analysis_options.yaml b/grpc/analysis_options.yaml new file mode 100644 index 0000000..6ffed43 --- /dev/null +++ b/grpc/analysis_options.yaml
@@ -0,0 +1,56 @@ +# Lint rules and documentation, see http://dart-lang.github.io/linter/lints + +linter: + rules: + - always_declare_return_types + - always_require_non_null_named_parameters + - annotate_overrides + - avoid_empty_else + - avoid_init_to_null + - avoid_null_checks_in_equality_operators + - avoid_relative_lib_imports + - avoid_return_types_on_setters + - avoid_shadowing_type_parameters + - avoid_types_as_parameter_names + - camel_case_extensions + - cancel_subscriptions + - close_sinks + - curly_braces_in_flow_control_structures + - directives_ordering + - empty_catches + - empty_constructor_bodies + - hash_and_equals + - iterable_contains_unrelated_type + - library_names + - library_prefixes + - list_remove_unrelated_type + - no_duplicate_case_values + - null_closures + - omit_local_variable_types + - prefer_adjacent_string_concatenation + - prefer_collection_literals + - prefer_conditional_assignment + - prefer_contains + - prefer_equal_for_default_values + - prefer_final_fields + - prefer_final_locals + - prefer_for_elements_to_map_fromIterable + - prefer_generic_function_type_aliases + - prefer_if_null_operators + - prefer_is_empty + - prefer_is_not_empty + - prefer_iterable_whereType + - prefer_single_quotes + - prefer_spread_collections + - recursive_getters + - slash_for_doc_comments + - test_types_in_equals + - type_init_formals + - unnecessary_const + - unnecessary_new + - unnecessary_null_in_if_null_operators + - unnecessary_this + - unrelated_type_equality_checks + - use_function_type_syntax_for_parameters + - use_rethrow_when_possible + - valid_regexps
diff --git a/grpc/build.yaml b/grpc/build.yaml new file mode 100644 index 0000000..2a9825b --- /dev/null +++ b/grpc/build.yaml
@@ -0,0 +1,5 @@ +targets: + $default: + sources: + exclude: + - example/**
diff --git a/grpc/example/README.md b/grpc/example/README.md new file mode 100644 index 0000000..67cc944 --- /dev/null +++ b/grpc/example/README.md
@@ -0,0 +1,15 @@ +Four code examples are available: + +1. [helloworld](https://github.com/grpc/grpc-dart/tree/master/example/helloworld): + A demonstration of using the Dart gRPC library to perform unary RPs. + +1. [googleapis](https://github.com/grpc/grpc-dart/tree/master/example/googleapis): + A demonstration of using the Dart gRPC library to communicate with Google APIs. + +1. [metadata](https://github.com/grpc/grpc-dart/tree/master/example/metadata): + A demonstration of how to handle custom metadata, cancellation, and timeouts in Dart gRPC. + +1. [route_guide](https://github.com/grpc/grpc-dart/tree/master/example/route_guide): + A demonstration of how to perform unary, client streaming, server streaming and full duplex RPCs. + +For a complete, step-wise working example, see the [Dart gRPC Quick Start](https://grpc.io/docs/quickstart/dart).
diff --git a/grpc/example/googleapis/README.md b/grpc/example/googleapis/README.md new file mode 100644 index 0000000..d78b5af --- /dev/null +++ b/grpc/example/googleapis/README.md
@@ -0,0 +1,62 @@ +# Description +The googleapis client demonstrates how to use Dart gRPC libraries to communicate +with Google APIs. + +# Set up Google Cloud Platform project +This example uses the Stackdriver Logging API. Please follow the documentation on +[Stackdriver Logging Documentation](https://cloud.google.com/logging/docs/) to create +a project and enable the logging API. + +Then follow the documentation to +[create a service account](https://developers.google.com/identity/protocols/OAuth2ServiceAccount#creatinganaccount). +This example uses the Logging/Logs Writer role. + +Create a new service key, download the JSON file for it, and save it as +`logging-service-account.json`. + +# Run the sample code +To run the example, assuming you are in the root of the googleapis folder, i.e., +.../example/googleapis/, first get the dependencies by running: + +```sh +$ pub get +``` + +Then, to run the logging client sample: + +```sh +$ pub run googleapis:logging +``` + +# Regenerate the stubs +The Dart gRPC stubs and message classes are generated based on protobuf definition +files from [googleapis/googleapis](https://github.com/googleapis/googleapis). + +To regenerate them, you will need to check out both +[googleapis/googleapis](https://github.com/googleapis/googleapis) and +[google/protobuf](https://github.com/google/protobuf). + +You will also need to have protoc version 3.0.0 or higher and the Dart protoc +plugin version 0.7.9 or higher on your PATH. + +To install protoc, see the instructions on +[the Protocol Buffers website](https://developers.google.com/protocol-buffers/). + +The easiest way to get the Dart protoc plugin is by running + +```sh +$ pub global activate protoc_plugin +``` + +and follow the directions to add `~/.pub-cache/bin` to your PATH, if you haven't +already done so. + +You can now regenerate the Dart files. Set the `PROTOBUF` and `GOOGLEAPIS` +environment variables to point to your clone of +[google/protobuf](https://github.com/google/protobuf) and +[googleapis/googleapis](https://github.com/googleapis/googleapis), respectively, +and then run + +```sh +$ tool/regenerate.sh +```
diff --git a/grpc/example/googleapis/analysis_options.yaml b/grpc/example/googleapis/analysis_options.yaml new file mode 100644 index 0000000..d8941fc --- /dev/null +++ b/grpc/example/googleapis/analysis_options.yaml
@@ -0,0 +1,5 @@ +include: ../../analysis_options.yaml + +linter: + rules: + directives_ordering: false
diff --git a/grpc/example/googleapis/bin/logging.dart b/grpc/example/googleapis/bin/logging.dart new file mode 100644 index 0000000..b3039f2 --- /dev/null +++ b/grpc/example/googleapis/bin/logging.dart
@@ -0,0 +1,56 @@ +// Copyright (c) 2017, the gRPC project authors. Please see the AUTHORS file +// for details. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import 'dart:async'; +import 'dart:io'; + +import 'package:grpc/grpc.dart'; + +import 'package:googleapis/src/generated/google/api/monitored_resource.pb.dart'; +import 'package:googleapis/src/generated/google/logging/type/log_severity.pb.dart'; +import 'package:googleapis/src/generated/google/logging/v2/log_entry.pb.dart'; +import 'package:googleapis/src/generated/google/logging/v2/logging.pbgrpc.dart'; + +Future<void> main() async { + final serviceAccountFile = File('logging-service-account.json'); + if (!serviceAccountFile.existsSync()) { + print('File logging-service-account.json not found. Please follow the ' + 'steps in README.md to create it.'); + exit(-1); + } + + final scopes = [ + 'https://www.googleapis.com/auth/cloud-platform', + 'https://www.googleapis.com/auth/logging.write', + ]; + + final authenticator = ServiceAccountAuthenticator( + serviceAccountFile.readAsStringSync(), scopes); + final projectId = authenticator.projectId; + + final channel = ClientChannel('logging.googleapis.com'); + final logging = + LoggingServiceV2Client(channel, options: authenticator.toCallOptions); + + final request = WriteLogEntriesRequest() + ..entries.add(LogEntry() + ..logName = 'projects/$projectId/logs/example' + ..severity = LogSeverity.INFO + ..resource = (MonitoredResource()..type = 'global') + ..textPayload = 'This is a log entry!'); + await logging.writeLogEntries(request); + + await channel.shutdown(); +}
diff --git a/grpc/example/googleapis/lib/src/generated/google/api/label.pb.dart b/grpc/example/googleapis/lib/src/generated/google/api/label.pb.dart new file mode 100644 index 0000000..ef6a9fa --- /dev/null +++ b/grpc/example/googleapis/lib/src/generated/google/api/label.pb.dart
@@ -0,0 +1,127 @@ +/// +// Generated code. Do not modify. +// source: google/api/label.proto +// +// @dart = 2.7 +// ignore_for_file: annotate_overrides,camel_case_types,unnecessary_const,non_constant_identifier_names,library_prefixes,unused_import,unused_shown_name,return_of_invalid_type,unnecessary_this,prefer_final_fields + +import 'dart:core' as $core; + +import 'package:protobuf/protobuf.dart' as $pb; + +import 'label.pbenum.dart'; + +export 'label.pbenum.dart'; + +class LabelDescriptor extends $pb.GeneratedMessage { + static final $pb.BuilderInfo _i = $pb.BuilderInfo( + const $core.bool.fromEnvironment('protobuf.omit_message_names') + ? '' + : 'LabelDescriptor', + package: const $pb.PackageName( + const $core.bool.fromEnvironment('protobuf.omit_message_names') + ? '' + : 'google.api'), + createEmptyInstance: create) + ..aOS( + 1, + const $core.bool.fromEnvironment('protobuf.omit_field_names') + ? '' + : 'key') + ..e<LabelDescriptor_ValueType>( + 2, + const $core.bool.fromEnvironment('protobuf.omit_field_names') + ? '' + : 'valueType', + $pb.PbFieldType.OE, + defaultOrMaker: LabelDescriptor_ValueType.STRING, + valueOf: LabelDescriptor_ValueType.valueOf, + enumValues: LabelDescriptor_ValueType.values) + ..aOS( + 3, + const $core.bool.fromEnvironment('protobuf.omit_field_names') + ? '' + : 'description') + ..hasRequiredFields = false; + + LabelDescriptor._() : super(); + factory LabelDescriptor({ + $core.String key, + LabelDescriptor_ValueType valueType, + $core.String description, + }) { + final _result = create(); + if (key != null) { + _result.key = key; + } + if (valueType != null) { + _result.valueType = valueType; + } + if (description != null) { + _result.description = description; + } + return _result; + } + factory LabelDescriptor.fromBuffer($core.List<$core.int> i, + [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromBuffer(i, r); + factory LabelDescriptor.fromJson($core.String i, + [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromJson(i, r); + @$core.Deprecated('Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.deepCopy] instead. ' + 'Will be removed in next major version') + LabelDescriptor clone() => LabelDescriptor()..mergeFromMessage(this); + @$core.Deprecated('Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.rebuild] instead. ' + 'Will be removed in next major version') + LabelDescriptor copyWith(void Function(LabelDescriptor) updates) => + super.copyWith((message) => + updates(message as LabelDescriptor)); // ignore: deprecated_member_use + $pb.BuilderInfo get info_ => _i; + @$core.pragma('dart2js:noInline') + static LabelDescriptor create() => LabelDescriptor._(); + LabelDescriptor createEmptyInstance() => create(); + static $pb.PbList<LabelDescriptor> createRepeated() => + $pb.PbList<LabelDescriptor>(); + @$core.pragma('dart2js:noInline') + static LabelDescriptor getDefault() => _defaultInstance ??= + $pb.GeneratedMessage.$_defaultFor<LabelDescriptor>(create); + static LabelDescriptor _defaultInstance; + + @$pb.TagNumber(1) + $core.String get key => $_getSZ(0); + @$pb.TagNumber(1) + set key($core.String v) { + $_setString(0, v); + } + + @$pb.TagNumber(1) + $core.bool hasKey() => $_has(0); + @$pb.TagNumber(1) + void clearKey() => clearField(1); + + @$pb.TagNumber(2) + LabelDescriptor_ValueType get valueType => $_getN(1); + @$pb.TagNumber(2) + set valueType(LabelDescriptor_ValueType v) { + setField(2, v); + } + + @$pb.TagNumber(2) + $core.bool hasValueType() => $_has(1); + @$pb.TagNumber(2) + void clearValueType() => clearField(2); + + @$pb.TagNumber(3) + $core.String get description => $_getSZ(2); + @$pb.TagNumber(3) + set description($core.String v) { + $_setString(2, v); + } + + @$pb.TagNumber(3) + $core.bool hasDescription() => $_has(2); + @$pb.TagNumber(3) + void clearDescription() => clearField(3); +}
diff --git a/grpc/example/googleapis/lib/src/generated/google/api/label.pbenum.dart b/grpc/example/googleapis/lib/src/generated/google/api/label.pbenum.dart new file mode 100644 index 0000000..e4ee606 --- /dev/null +++ b/grpc/example/googleapis/lib/src/generated/google/api/label.pbenum.dart
@@ -0,0 +1,41 @@ +/// +// Generated code. Do not modify. +// source: google/api/label.proto +// +// @dart = 2.7 +// ignore_for_file: annotate_overrides,camel_case_types,unnecessary_const,non_constant_identifier_names,library_prefixes,unused_import,unused_shown_name,return_of_invalid_type,unnecessary_this,prefer_final_fields + +// ignore_for_file: UNDEFINED_SHOWN_NAME +import 'dart:core' as $core; +import 'package:protobuf/protobuf.dart' as $pb; + +class LabelDescriptor_ValueType extends $pb.ProtobufEnum { + static const LabelDescriptor_ValueType STRING = LabelDescriptor_ValueType._( + 0, + const $core.bool.fromEnvironment('protobuf.omit_enum_names') + ? '' + : 'STRING'); + static const LabelDescriptor_ValueType BOOL = LabelDescriptor_ValueType._( + 1, + const $core.bool.fromEnvironment('protobuf.omit_enum_names') + ? '' + : 'BOOL'); + static const LabelDescriptor_ValueType INT64 = LabelDescriptor_ValueType._( + 2, + const $core.bool.fromEnvironment('protobuf.omit_enum_names') + ? '' + : 'INT64'); + + static const $core.List<LabelDescriptor_ValueType> values = + <LabelDescriptor_ValueType>[ + STRING, + BOOL, + INT64, + ]; + + static final $core.Map<$core.int, LabelDescriptor_ValueType> _byValue = + $pb.ProtobufEnum.initByValue(values); + static LabelDescriptor_ValueType valueOf($core.int value) => _byValue[value]; + + const LabelDescriptor_ValueType._($core.int v, $core.String n) : super(v, n); +}
diff --git a/grpc/example/googleapis/lib/src/generated/google/api/label.pbjson.dart b/grpc/example/googleapis/lib/src/generated/google/api/label.pbjson.dart new file mode 100644 index 0000000..65c2ce8 --- /dev/null +++ b/grpc/example/googleapis/lib/src/generated/google/api/label.pbjson.dart
@@ -0,0 +1,32 @@ +/// +// Generated code. Do not modify. +// source: google/api/label.proto +// +// @dart = 2.7 +// ignore_for_file: annotate_overrides,camel_case_types,unnecessary_const,non_constant_identifier_names,library_prefixes,unused_import,unused_shown_name,return_of_invalid_type,unnecessary_this,prefer_final_fields + +const LabelDescriptor$json = const { + '1': 'LabelDescriptor', + '2': const [ + const {'1': 'key', '3': 1, '4': 1, '5': 9, '10': 'key'}, + const { + '1': 'value_type', + '3': 2, + '4': 1, + '5': 14, + '6': '.google.api.LabelDescriptor.ValueType', + '10': 'valueType' + }, + const {'1': 'description', '3': 3, '4': 1, '5': 9, '10': 'description'}, + ], + '4': const [LabelDescriptor_ValueType$json], +}; + +const LabelDescriptor_ValueType$json = const { + '1': 'ValueType', + '2': const [ + const {'1': 'STRING', '2': 0}, + const {'1': 'BOOL', '2': 1}, + const {'1': 'INT64', '2': 2}, + ], +};
diff --git a/grpc/example/googleapis/lib/src/generated/google/api/launch_stage.pb.dart b/grpc/example/googleapis/lib/src/generated/google/api/launch_stage.pb.dart new file mode 100644 index 0000000..7d65cee --- /dev/null +++ b/grpc/example/googleapis/lib/src/generated/google/api/launch_stage.pb.dart
@@ -0,0 +1,10 @@ +/// +// Generated code. Do not modify. +// source: google/api/launch_stage.proto +// +// @dart = 2.7 +// ignore_for_file: annotate_overrides,camel_case_types,unnecessary_const,non_constant_identifier_names,library_prefixes,unused_import,unused_shown_name,return_of_invalid_type,unnecessary_this,prefer_final_fields + +import 'dart:core' as $core; + +export 'launch_stage.pbenum.dart';
diff --git a/grpc/example/googleapis/lib/src/generated/google/api/launch_stage.pbenum.dart b/grpc/example/googleapis/lib/src/generated/google/api/launch_stage.pbenum.dart new file mode 100644 index 0000000..3a8cc21 --- /dev/null +++ b/grpc/example/googleapis/lib/src/generated/google/api/launch_stage.pbenum.dart
@@ -0,0 +1,67 @@ +/// +// Generated code. Do not modify. +// source: google/api/launch_stage.proto +// +// @dart = 2.7 +// ignore_for_file: annotate_overrides,camel_case_types,unnecessary_const,non_constant_identifier_names,library_prefixes,unused_import,unused_shown_name,return_of_invalid_type,unnecessary_this,prefer_final_fields + +// ignore_for_file: UNDEFINED_SHOWN_NAME +import 'dart:core' as $core; +import 'package:protobuf/protobuf.dart' as $pb; + +class LaunchStage extends $pb.ProtobufEnum { + static const LaunchStage LAUNCH_STAGE_UNSPECIFIED = LaunchStage._( + 0, + const $core.bool.fromEnvironment('protobuf.omit_enum_names') + ? '' + : 'LAUNCH_STAGE_UNSPECIFIED'); + static const LaunchStage UNIMPLEMENTED = LaunchStage._( + 6, + const $core.bool.fromEnvironment('protobuf.omit_enum_names') + ? '' + : 'UNIMPLEMENTED'); + static const LaunchStage PRELAUNCH = LaunchStage._( + 7, + const $core.bool.fromEnvironment('protobuf.omit_enum_names') + ? '' + : 'PRELAUNCH'); + static const LaunchStage EARLY_ACCESS = LaunchStage._( + 1, + const $core.bool.fromEnvironment('protobuf.omit_enum_names') + ? '' + : 'EARLY_ACCESS'); + static const LaunchStage ALPHA = LaunchStage._( + 2, + const $core.bool.fromEnvironment('protobuf.omit_enum_names') + ? '' + : 'ALPHA'); + static const LaunchStage BETA = LaunchStage._( + 3, + const $core.bool.fromEnvironment('protobuf.omit_enum_names') + ? '' + : 'BETA'); + static const LaunchStage GA = LaunchStage._(4, + const $core.bool.fromEnvironment('protobuf.omit_enum_names') ? '' : 'GA'); + static const LaunchStage DEPRECATED = LaunchStage._( + 5, + const $core.bool.fromEnvironment('protobuf.omit_enum_names') + ? '' + : 'DEPRECATED'); + + static const $core.List<LaunchStage> values = <LaunchStage>[ + LAUNCH_STAGE_UNSPECIFIED, + UNIMPLEMENTED, + PRELAUNCH, + EARLY_ACCESS, + ALPHA, + BETA, + GA, + DEPRECATED, + ]; + + static final $core.Map<$core.int, LaunchStage> _byValue = + $pb.ProtobufEnum.initByValue(values); + static LaunchStage valueOf($core.int value) => _byValue[value]; + + const LaunchStage._($core.int v, $core.String n) : super(v, n); +}
diff --git a/grpc/example/googleapis/lib/src/generated/google/api/launch_stage.pbjson.dart b/grpc/example/googleapis/lib/src/generated/google/api/launch_stage.pbjson.dart new file mode 100644 index 0000000..0b3400e --- /dev/null +++ b/grpc/example/googleapis/lib/src/generated/google/api/launch_stage.pbjson.dart
@@ -0,0 +1,20 @@ +/// +// Generated code. Do not modify. +// source: google/api/launch_stage.proto +// +// @dart = 2.7 +// ignore_for_file: annotate_overrides,camel_case_types,unnecessary_const,non_constant_identifier_names,library_prefixes,unused_import,unused_shown_name,return_of_invalid_type,unnecessary_this,prefer_final_fields + +const LaunchStage$json = const { + '1': 'LaunchStage', + '2': const [ + const {'1': 'LAUNCH_STAGE_UNSPECIFIED', '2': 0}, + const {'1': 'UNIMPLEMENTED', '2': 6}, + const {'1': 'PRELAUNCH', '2': 7}, + const {'1': 'EARLY_ACCESS', '2': 1}, + const {'1': 'ALPHA', '2': 2}, + const {'1': 'BETA', '2': 3}, + const {'1': 'GA', '2': 4}, + const {'1': 'DEPRECATED', '2': 5}, + ], +};
diff --git a/grpc/example/googleapis/lib/src/generated/google/api/monitored_resource.pb.dart b/grpc/example/googleapis/lib/src/generated/google/api/monitored_resource.pb.dart new file mode 100644 index 0000000..1c90033 --- /dev/null +++ b/grpc/example/googleapis/lib/src/generated/google/api/monitored_resource.pb.dart
@@ -0,0 +1,358 @@ +/// +// Generated code. Do not modify. +// source: google/api/monitored_resource.proto +// +// @dart = 2.7 +// ignore_for_file: annotate_overrides,camel_case_types,unnecessary_const,non_constant_identifier_names,library_prefixes,unused_import,unused_shown_name,return_of_invalid_type,unnecessary_this,prefer_final_fields + +import 'dart:core' as $core; + +import 'package:protobuf/protobuf.dart' as $pb; + +import 'label.pb.dart' as $0; +import '../protobuf/struct.pb.dart' as $1; + +import 'launch_stage.pbenum.dart' as $2; + +class MonitoredResourceDescriptor extends $pb.GeneratedMessage { + static final $pb.BuilderInfo _i = $pb.BuilderInfo( + const $core.bool.fromEnvironment('protobuf.omit_message_names') + ? '' + : 'MonitoredResourceDescriptor', + package: const $pb.PackageName( + const $core.bool.fromEnvironment('protobuf.omit_message_names') + ? '' + : 'google.api'), + createEmptyInstance: create) + ..aOS( + 1, + const $core.bool.fromEnvironment('protobuf.omit_field_names') + ? '' + : 'type') + ..aOS( + 2, + const $core.bool.fromEnvironment('protobuf.omit_field_names') + ? '' + : 'displayName') + ..aOS( + 3, + const $core.bool.fromEnvironment('protobuf.omit_field_names') + ? '' + : 'description') + ..pc<$0.LabelDescriptor>( + 4, + const $core.bool.fromEnvironment('protobuf.omit_field_names') + ? '' + : 'labels', + $pb.PbFieldType.PM, + subBuilder: $0.LabelDescriptor.create) + ..aOS( + 5, + const $core.bool.fromEnvironment('protobuf.omit_field_names') + ? '' + : 'name') + ..e<$2.LaunchStage>( + 7, + const $core.bool.fromEnvironment('protobuf.omit_field_names') + ? '' + : 'launchStage', + $pb.PbFieldType.OE, + defaultOrMaker: $2.LaunchStage.LAUNCH_STAGE_UNSPECIFIED, + valueOf: $2.LaunchStage.valueOf, + enumValues: $2.LaunchStage.values) + ..hasRequiredFields = false; + + MonitoredResourceDescriptor._() : super(); + factory MonitoredResourceDescriptor({ + $core.String type, + $core.String displayName, + $core.String description, + $core.Iterable<$0.LabelDescriptor> labels, + $core.String name, + $2.LaunchStage launchStage, + }) { + final _result = create(); + if (type != null) { + _result.type = type; + } + if (displayName != null) { + _result.displayName = displayName; + } + if (description != null) { + _result.description = description; + } + if (labels != null) { + _result.labels.addAll(labels); + } + if (name != null) { + _result.name = name; + } + if (launchStage != null) { + _result.launchStage = launchStage; + } + return _result; + } + factory MonitoredResourceDescriptor.fromBuffer($core.List<$core.int> i, + [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromBuffer(i, r); + factory MonitoredResourceDescriptor.fromJson($core.String i, + [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromJson(i, r); + @$core.Deprecated('Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.deepCopy] instead. ' + 'Will be removed in next major version') + MonitoredResourceDescriptor clone() => + MonitoredResourceDescriptor()..mergeFromMessage(this); + @$core.Deprecated('Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.rebuild] instead. ' + 'Will be removed in next major version') + MonitoredResourceDescriptor copyWith( + void Function(MonitoredResourceDescriptor) updates) => + super.copyWith((message) => updates(message + as MonitoredResourceDescriptor)); // ignore: deprecated_member_use + $pb.BuilderInfo get info_ => _i; + @$core.pragma('dart2js:noInline') + static MonitoredResourceDescriptor create() => + MonitoredResourceDescriptor._(); + MonitoredResourceDescriptor createEmptyInstance() => create(); + static $pb.PbList<MonitoredResourceDescriptor> createRepeated() => + $pb.PbList<MonitoredResourceDescriptor>(); + @$core.pragma('dart2js:noInline') + static MonitoredResourceDescriptor getDefault() => _defaultInstance ??= + $pb.GeneratedMessage.$_defaultFor<MonitoredResourceDescriptor>(create); + static MonitoredResourceDescriptor _defaultInstance; + + @$pb.TagNumber(1) + $core.String get type => $_getSZ(0); + @$pb.TagNumber(1) + set type($core.String v) { + $_setString(0, v); + } + + @$pb.TagNumber(1) + $core.bool hasType() => $_has(0); + @$pb.TagNumber(1) + void clearType() => clearField(1); + + @$pb.TagNumber(2) + $core.String get displayName => $_getSZ(1); + @$pb.TagNumber(2) + set displayName($core.String v) { + $_setString(1, v); + } + + @$pb.TagNumber(2) + $core.bool hasDisplayName() => $_has(1); + @$pb.TagNumber(2) + void clearDisplayName() => clearField(2); + + @$pb.TagNumber(3) + $core.String get description => $_getSZ(2); + @$pb.TagNumber(3) + set description($core.String v) { + $_setString(2, v); + } + + @$pb.TagNumber(3) + $core.bool hasDescription() => $_has(2); + @$pb.TagNumber(3) + void clearDescription() => clearField(3); + + @$pb.TagNumber(4) + $core.List<$0.LabelDescriptor> get labels => $_getList(3); + + @$pb.TagNumber(5) + $core.String get name => $_getSZ(4); + @$pb.TagNumber(5) + set name($core.String v) { + $_setString(4, v); + } + + @$pb.TagNumber(5) + $core.bool hasName() => $_has(4); + @$pb.TagNumber(5) + void clearName() => clearField(5); + + @$pb.TagNumber(7) + $2.LaunchStage get launchStage => $_getN(5); + @$pb.TagNumber(7) + set launchStage($2.LaunchStage v) { + setField(7, v); + } + + @$pb.TagNumber(7) + $core.bool hasLaunchStage() => $_has(5); + @$pb.TagNumber(7) + void clearLaunchStage() => clearField(7); +} + +class MonitoredResource extends $pb.GeneratedMessage { + static final $pb.BuilderInfo _i = $pb.BuilderInfo( + const $core.bool.fromEnvironment('protobuf.omit_message_names') + ? '' + : 'MonitoredResource', + package: const $pb.PackageName( + const $core.bool.fromEnvironment('protobuf.omit_message_names') + ? '' + : 'google.api'), + createEmptyInstance: create) + ..aOS( + 1, + const $core.bool.fromEnvironment('protobuf.omit_field_names') + ? '' + : 'type') + ..m<$core.String, $core.String>( + 2, + const $core.bool.fromEnvironment('protobuf.omit_field_names') + ? '' + : 'labels', + entryClassName: 'MonitoredResource.LabelsEntry', + keyFieldType: $pb.PbFieldType.OS, + valueFieldType: $pb.PbFieldType.OS, + packageName: const $pb.PackageName('google.api')) + ..hasRequiredFields = false; + + MonitoredResource._() : super(); + factory MonitoredResource({ + $core.String type, + $core.Map<$core.String, $core.String> labels, + }) { + final _result = create(); + if (type != null) { + _result.type = type; + } + if (labels != null) { + _result.labels.addAll(labels); + } + return _result; + } + factory MonitoredResource.fromBuffer($core.List<$core.int> i, + [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromBuffer(i, r); + factory MonitoredResource.fromJson($core.String i, + [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromJson(i, r); + @$core.Deprecated('Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.deepCopy] instead. ' + 'Will be removed in next major version') + MonitoredResource clone() => MonitoredResource()..mergeFromMessage(this); + @$core.Deprecated('Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.rebuild] instead. ' + 'Will be removed in next major version') + MonitoredResource copyWith(void Function(MonitoredResource) updates) => + super.copyWith((message) => updates( + message as MonitoredResource)); // ignore: deprecated_member_use + $pb.BuilderInfo get info_ => _i; + @$core.pragma('dart2js:noInline') + static MonitoredResource create() => MonitoredResource._(); + MonitoredResource createEmptyInstance() => create(); + static $pb.PbList<MonitoredResource> createRepeated() => + $pb.PbList<MonitoredResource>(); + @$core.pragma('dart2js:noInline') + static MonitoredResource getDefault() => _defaultInstance ??= + $pb.GeneratedMessage.$_defaultFor<MonitoredResource>(create); + static MonitoredResource _defaultInstance; + + @$pb.TagNumber(1) + $core.String get type => $_getSZ(0); + @$pb.TagNumber(1) + set type($core.String v) { + $_setString(0, v); + } + + @$pb.TagNumber(1) + $core.bool hasType() => $_has(0); + @$pb.TagNumber(1) + void clearType() => clearField(1); + + @$pb.TagNumber(2) + $core.Map<$core.String, $core.String> get labels => $_getMap(1); +} + +class MonitoredResourceMetadata extends $pb.GeneratedMessage { + static final $pb.BuilderInfo _i = $pb.BuilderInfo( + const $core.bool.fromEnvironment('protobuf.omit_message_names') + ? '' + : 'MonitoredResourceMetadata', + package: const $pb.PackageName( + const $core.bool.fromEnvironment('protobuf.omit_message_names') + ? '' + : 'google.api'), + createEmptyInstance: create) + ..aOM<$1.Struct>( + 1, + const $core.bool.fromEnvironment('protobuf.omit_field_names') + ? '' + : 'systemLabels', + subBuilder: $1.Struct.create) + ..m<$core.String, $core.String>( + 2, + const $core.bool.fromEnvironment('protobuf.omit_field_names') + ? '' + : 'userLabels', + entryClassName: 'MonitoredResourceMetadata.UserLabelsEntry', + keyFieldType: $pb.PbFieldType.OS, + valueFieldType: $pb.PbFieldType.OS, + packageName: const $pb.PackageName('google.api')) + ..hasRequiredFields = false; + + MonitoredResourceMetadata._() : super(); + factory MonitoredResourceMetadata({ + $1.Struct systemLabels, + $core.Map<$core.String, $core.String> userLabels, + }) { + final _result = create(); + if (systemLabels != null) { + _result.systemLabels = systemLabels; + } + if (userLabels != null) { + _result.userLabels.addAll(userLabels); + } + return _result; + } + factory MonitoredResourceMetadata.fromBuffer($core.List<$core.int> i, + [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromBuffer(i, r); + factory MonitoredResourceMetadata.fromJson($core.String i, + [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromJson(i, r); + @$core.Deprecated('Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.deepCopy] instead. ' + 'Will be removed in next major version') + MonitoredResourceMetadata clone() => + MonitoredResourceMetadata()..mergeFromMessage(this); + @$core.Deprecated('Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.rebuild] instead. ' + 'Will be removed in next major version') + MonitoredResourceMetadata copyWith( + void Function(MonitoredResourceMetadata) updates) => + super.copyWith((message) => updates(message + as MonitoredResourceMetadata)); // ignore: deprecated_member_use + $pb.BuilderInfo get info_ => _i; + @$core.pragma('dart2js:noInline') + static MonitoredResourceMetadata create() => MonitoredResourceMetadata._(); + MonitoredResourceMetadata createEmptyInstance() => create(); + static $pb.PbList<MonitoredResourceMetadata> createRepeated() => + $pb.PbList<MonitoredResourceMetadata>(); + @$core.pragma('dart2js:noInline') + static MonitoredResourceMetadata getDefault() => _defaultInstance ??= + $pb.GeneratedMessage.$_defaultFor<MonitoredResourceMetadata>(create); + static MonitoredResourceMetadata _defaultInstance; + + @$pb.TagNumber(1) + $1.Struct get systemLabels => $_getN(0); + @$pb.TagNumber(1) + set systemLabels($1.Struct v) { + setField(1, v); + } + + @$pb.TagNumber(1) + $core.bool hasSystemLabels() => $_has(0); + @$pb.TagNumber(1) + void clearSystemLabels() => clearField(1); + @$pb.TagNumber(1) + $1.Struct ensureSystemLabels() => $_ensure(0); + + @$pb.TagNumber(2) + $core.Map<$core.String, $core.String> get userLabels => $_getMap(1); +}
diff --git a/grpc/example/googleapis/lib/src/generated/google/api/monitored_resource.pbenum.dart b/grpc/example/googleapis/lib/src/generated/google/api/monitored_resource.pbenum.dart new file mode 100644 index 0000000..b0331cc --- /dev/null +++ b/grpc/example/googleapis/lib/src/generated/google/api/monitored_resource.pbenum.dart
@@ -0,0 +1,6 @@ +/// +// Generated code. Do not modify. +// source: google/api/monitored_resource.proto +// +// @dart = 2.7 +// ignore_for_file: annotate_overrides,camel_case_types,unnecessary_const,non_constant_identifier_names,library_prefixes,unused_import,unused_shown_name,return_of_invalid_type,unnecessary_this,prefer_final_fields
diff --git a/grpc/example/googleapis/lib/src/generated/google/api/monitored_resource.pbjson.dart b/grpc/example/googleapis/lib/src/generated/google/api/monitored_resource.pbjson.dart new file mode 100644 index 0000000..9a24f95 --- /dev/null +++ b/grpc/example/googleapis/lib/src/generated/google/api/monitored_resource.pbjson.dart
@@ -0,0 +1,89 @@ +/// +// Generated code. Do not modify. +// source: google/api/monitored_resource.proto +// +// @dart = 2.7 +// ignore_for_file: annotate_overrides,camel_case_types,unnecessary_const,non_constant_identifier_names,library_prefixes,unused_import,unused_shown_name,return_of_invalid_type,unnecessary_this,prefer_final_fields + +const MonitoredResourceDescriptor$json = const { + '1': 'MonitoredResourceDescriptor', + '2': const [ + const {'1': 'name', '3': 5, '4': 1, '5': 9, '10': 'name'}, + const {'1': 'type', '3': 1, '4': 1, '5': 9, '10': 'type'}, + const {'1': 'display_name', '3': 2, '4': 1, '5': 9, '10': 'displayName'}, + const {'1': 'description', '3': 3, '4': 1, '5': 9, '10': 'description'}, + const { + '1': 'labels', + '3': 4, + '4': 3, + '5': 11, + '6': '.google.api.LabelDescriptor', + '10': 'labels' + }, + const { + '1': 'launch_stage', + '3': 7, + '4': 1, + '5': 14, + '6': '.google.api.LaunchStage', + '10': 'launchStage' + }, + ], +}; + +const MonitoredResource$json = const { + '1': 'MonitoredResource', + '2': const [ + const {'1': 'type', '3': 1, '4': 1, '5': 9, '10': 'type'}, + const { + '1': 'labels', + '3': 2, + '4': 3, + '5': 11, + '6': '.google.api.MonitoredResource.LabelsEntry', + '10': 'labels' + }, + ], + '3': const [MonitoredResource_LabelsEntry$json], +}; + +const MonitoredResource_LabelsEntry$json = const { + '1': 'LabelsEntry', + '2': const [ + const {'1': 'key', '3': 1, '4': 1, '5': 9, '10': 'key'}, + const {'1': 'value', '3': 2, '4': 1, '5': 9, '10': 'value'}, + ], + '7': const {'7': true}, +}; + +const MonitoredResourceMetadata$json = const { + '1': 'MonitoredResourceMetadata', + '2': const [ + const { + '1': 'system_labels', + '3': 1, + '4': 1, + '5': 11, + '6': '.google.protobuf.Struct', + '10': 'systemLabels' + }, + const { + '1': 'user_labels', + '3': 2, + '4': 3, + '5': 11, + '6': '.google.api.MonitoredResourceMetadata.UserLabelsEntry', + '10': 'userLabels' + }, + ], + '3': const [MonitoredResourceMetadata_UserLabelsEntry$json], +}; + +const MonitoredResourceMetadata_UserLabelsEntry$json = const { + '1': 'UserLabelsEntry', + '2': const [ + const {'1': 'key', '3': 1, '4': 1, '5': 9, '10': 'key'}, + const {'1': 'value', '3': 2, '4': 1, '5': 9, '10': 'value'}, + ], + '7': const {'7': true}, +};
diff --git a/grpc/example/googleapis/lib/src/generated/google/logging/type/http_request.pb.dart b/grpc/example/googleapis/lib/src/generated/google/logging/type/http_request.pb.dart new file mode 100644 index 0000000..bb1017c --- /dev/null +++ b/grpc/example/googleapis/lib/src/generated/google/logging/type/http_request.pb.dart
@@ -0,0 +1,377 @@ +/// +// Generated code. Do not modify. +// source: google/logging/type/http_request.proto +// +// @dart = 2.7 +// ignore_for_file: annotate_overrides,camel_case_types,unnecessary_const,non_constant_identifier_names,library_prefixes,unused_import,unused_shown_name,return_of_invalid_type,unnecessary_this,prefer_final_fields + +import 'dart:core' as $core; + +import 'package:fixnum/fixnum.dart' as $fixnum; +import 'package:protobuf/protobuf.dart' as $pb; + +import '../../protobuf/duration.pb.dart' as $0; + +class HttpRequest extends $pb.GeneratedMessage { + static final $pb.BuilderInfo _i = $pb.BuilderInfo( + const $core.bool.fromEnvironment('protobuf.omit_message_names') + ? '' + : 'HttpRequest', + package: const $pb.PackageName( + const $core.bool.fromEnvironment('protobuf.omit_message_names') + ? '' + : 'google.logging.type'), + createEmptyInstance: create) + ..aOS( + 1, + const $core.bool.fromEnvironment('protobuf.omit_field_names') + ? '' + : 'requestMethod') + ..aOS( + 2, + const $core.bool.fromEnvironment('protobuf.omit_field_names') + ? '' + : 'requestUrl') + ..aInt64( + 3, + const $core.bool.fromEnvironment('protobuf.omit_field_names') + ? '' + : 'requestSize') + ..a<$core.int>( + 4, + const $core.bool.fromEnvironment('protobuf.omit_field_names') + ? '' + : 'status', + $pb.PbFieldType.O3) + ..aInt64( + 5, + const $core.bool.fromEnvironment('protobuf.omit_field_names') + ? '' + : 'responseSize') + ..aOS( + 6, + const $core.bool.fromEnvironment('protobuf.omit_field_names') + ? '' + : 'userAgent') + ..aOS( + 7, + const $core.bool.fromEnvironment('protobuf.omit_field_names') + ? '' + : 'remoteIp') + ..aOS( + 8, + const $core.bool.fromEnvironment('protobuf.omit_field_names') + ? '' + : 'referer') + ..aOB( + 9, + const $core.bool.fromEnvironment('protobuf.omit_field_names') + ? '' + : 'cacheHit') + ..aOB( + 10, + const $core.bool.fromEnvironment('protobuf.omit_field_names') + ? '' + : 'cacheValidatedWithOriginServer') + ..aOB( + 11, + const $core.bool.fromEnvironment('protobuf.omit_field_names') + ? '' + : 'cacheLookup') + ..aInt64( + 12, + const $core.bool.fromEnvironment('protobuf.omit_field_names') + ? '' + : 'cacheFillBytes') + ..aOS( + 13, + const $core.bool.fromEnvironment('protobuf.omit_field_names') + ? '' + : 'serverIp') + ..aOM<$0.Duration>( + 14, + const $core.bool.fromEnvironment('protobuf.omit_field_names') + ? '' + : 'latency', + subBuilder: $0.Duration.create) + ..aOS( + 15, + const $core.bool.fromEnvironment('protobuf.omit_field_names') + ? '' + : 'protocol') + ..hasRequiredFields = false; + + HttpRequest._() : super(); + factory HttpRequest({ + $core.String requestMethod, + $core.String requestUrl, + $fixnum.Int64 requestSize, + $core.int status, + $fixnum.Int64 responseSize, + $core.String userAgent, + $core.String remoteIp, + $core.String referer, + $core.bool cacheHit, + $core.bool cacheValidatedWithOriginServer, + $core.bool cacheLookup, + $fixnum.Int64 cacheFillBytes, + $core.String serverIp, + $0.Duration latency, + $core.String protocol, + }) { + final _result = create(); + if (requestMethod != null) { + _result.requestMethod = requestMethod; + } + if (requestUrl != null) { + _result.requestUrl = requestUrl; + } + if (requestSize != null) { + _result.requestSize = requestSize; + } + if (status != null) { + _result.status = status; + } + if (responseSize != null) { + _result.responseSize = responseSize; + } + if (userAgent != null) { + _result.userAgent = userAgent; + } + if (remoteIp != null) { + _result.remoteIp = remoteIp; + } + if (referer != null) { + _result.referer = referer; + } + if (cacheHit != null) { + _result.cacheHit = cacheHit; + } + if (cacheValidatedWithOriginServer != null) { + _result.cacheValidatedWithOriginServer = cacheValidatedWithOriginServer; + } + if (cacheLookup != null) { + _result.cacheLookup = cacheLookup; + } + if (cacheFillBytes != null) { + _result.cacheFillBytes = cacheFillBytes; + } + if (serverIp != null) { + _result.serverIp = serverIp; + } + if (latency != null) { + _result.latency = latency; + } + if (protocol != null) { + _result.protocol = protocol; + } + return _result; + } + factory HttpRequest.fromBuffer($core.List<$core.int> i, + [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromBuffer(i, r); + factory HttpRequest.fromJson($core.String i, + [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromJson(i, r); + @$core.Deprecated('Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.deepCopy] instead. ' + 'Will be removed in next major version') + HttpRequest clone() => HttpRequest()..mergeFromMessage(this); + @$core.Deprecated('Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.rebuild] instead. ' + 'Will be removed in next major version') + HttpRequest copyWith(void Function(HttpRequest) updates) => + super.copyWith((message) => + updates(message as HttpRequest)); // ignore: deprecated_member_use + $pb.BuilderInfo get info_ => _i; + @$core.pragma('dart2js:noInline') + static HttpRequest create() => HttpRequest._(); + HttpRequest createEmptyInstance() => create(); + static $pb.PbList<HttpRequest> createRepeated() => $pb.PbList<HttpRequest>(); + @$core.pragma('dart2js:noInline') + static HttpRequest getDefault() => _defaultInstance ??= + $pb.GeneratedMessage.$_defaultFor<HttpRequest>(create); + static HttpRequest _defaultInstance; + + @$pb.TagNumber(1) + $core.String get requestMethod => $_getSZ(0); + @$pb.TagNumber(1) + set requestMethod($core.String v) { + $_setString(0, v); + } + + @$pb.TagNumber(1) + $core.bool hasRequestMethod() => $_has(0); + @$pb.TagNumber(1) + void clearRequestMethod() => clearField(1); + + @$pb.TagNumber(2) + $core.String get requestUrl => $_getSZ(1); + @$pb.TagNumber(2) + set requestUrl($core.String v) { + $_setString(1, v); + } + + @$pb.TagNumber(2) + $core.bool hasRequestUrl() => $_has(1); + @$pb.TagNumber(2) + void clearRequestUrl() => clearField(2); + + @$pb.TagNumber(3) + $fixnum.Int64 get requestSize => $_getI64(2); + @$pb.TagNumber(3) + set requestSize($fixnum.Int64 v) { + $_setInt64(2, v); + } + + @$pb.TagNumber(3) + $core.bool hasRequestSize() => $_has(2); + @$pb.TagNumber(3) + void clearRequestSize() => clearField(3); + + @$pb.TagNumber(4) + $core.int get status => $_getIZ(3); + @$pb.TagNumber(4) + set status($core.int v) { + $_setSignedInt32(3, v); + } + + @$pb.TagNumber(4) + $core.bool hasStatus() => $_has(3); + @$pb.TagNumber(4) + void clearStatus() => clearField(4); + + @$pb.TagNumber(5) + $fixnum.Int64 get responseSize => $_getI64(4); + @$pb.TagNumber(5) + set responseSize($fixnum.Int64 v) { + $_setInt64(4, v); + } + + @$pb.TagNumber(5) + $core.bool hasResponseSize() => $_has(4); + @$pb.TagNumber(5) + void clearResponseSize() => clearField(5); + + @$pb.TagNumber(6) + $core.String get userAgent => $_getSZ(5); + @$pb.TagNumber(6) + set userAgent($core.String v) { + $_setString(5, v); + } + + @$pb.TagNumber(6) + $core.bool hasUserAgent() => $_has(5); + @$pb.TagNumber(6) + void clearUserAgent() => clearField(6); + + @$pb.TagNumber(7) + $core.String get remoteIp => $_getSZ(6); + @$pb.TagNumber(7) + set remoteIp($core.String v) { + $_setString(6, v); + } + + @$pb.TagNumber(7) + $core.bool hasRemoteIp() => $_has(6); + @$pb.TagNumber(7) + void clearRemoteIp() => clearField(7); + + @$pb.TagNumber(8) + $core.String get referer => $_getSZ(7); + @$pb.TagNumber(8) + set referer($core.String v) { + $_setString(7, v); + } + + @$pb.TagNumber(8) + $core.bool hasReferer() => $_has(7); + @$pb.TagNumber(8) + void clearReferer() => clearField(8); + + @$pb.TagNumber(9) + $core.bool get cacheHit => $_getBF(8); + @$pb.TagNumber(9) + set cacheHit($core.bool v) { + $_setBool(8, v); + } + + @$pb.TagNumber(9) + $core.bool hasCacheHit() => $_has(8); + @$pb.TagNumber(9) + void clearCacheHit() => clearField(9); + + @$pb.TagNumber(10) + $core.bool get cacheValidatedWithOriginServer => $_getBF(9); + @$pb.TagNumber(10) + set cacheValidatedWithOriginServer($core.bool v) { + $_setBool(9, v); + } + + @$pb.TagNumber(10) + $core.bool hasCacheValidatedWithOriginServer() => $_has(9); + @$pb.TagNumber(10) + void clearCacheValidatedWithOriginServer() => clearField(10); + + @$pb.TagNumber(11) + $core.bool get cacheLookup => $_getBF(10); + @$pb.TagNumber(11) + set cacheLookup($core.bool v) { + $_setBool(10, v); + } + + @$pb.TagNumber(11) + $core.bool hasCacheLookup() => $_has(10); + @$pb.TagNumber(11) + void clearCacheLookup() => clearField(11); + + @$pb.TagNumber(12) + $fixnum.Int64 get cacheFillBytes => $_getI64(11); + @$pb.TagNumber(12) + set cacheFillBytes($fixnum.Int64 v) { + $_setInt64(11, v); + } + + @$pb.TagNumber(12) + $core.bool hasCacheFillBytes() => $_has(11); + @$pb.TagNumber(12) + void clearCacheFillBytes() => clearField(12); + + @$pb.TagNumber(13) + $core.String get serverIp => $_getSZ(12); + @$pb.TagNumber(13) + set serverIp($core.String v) { + $_setString(12, v); + } + + @$pb.TagNumber(13) + $core.bool hasServerIp() => $_has(12); + @$pb.TagNumber(13) + void clearServerIp() => clearField(13); + + @$pb.TagNumber(14) + $0.Duration get latency => $_getN(13); + @$pb.TagNumber(14) + set latency($0.Duration v) { + setField(14, v); + } + + @$pb.TagNumber(14) + $core.bool hasLatency() => $_has(13); + @$pb.TagNumber(14) + void clearLatency() => clearField(14); + @$pb.TagNumber(14) + $0.Duration ensureLatency() => $_ensure(13); + + @$pb.TagNumber(15) + $core.String get protocol => $_getSZ(14); + @$pb.TagNumber(15) + set protocol($core.String v) { + $_setString(14, v); + } + + @$pb.TagNumber(15) + $core.bool hasProtocol() => $_has(14); + @$pb.TagNumber(15) + void clearProtocol() => clearField(15); +}
diff --git a/grpc/example/googleapis/lib/src/generated/google/logging/type/http_request.pbenum.dart b/grpc/example/googleapis/lib/src/generated/google/logging/type/http_request.pbenum.dart new file mode 100644 index 0000000..fede761 --- /dev/null +++ b/grpc/example/googleapis/lib/src/generated/google/logging/type/http_request.pbenum.dart
@@ -0,0 +1,6 @@ +/// +// Generated code. Do not modify. +// source: google/logging/type/http_request.proto +// +// @dart = 2.7 +// ignore_for_file: annotate_overrides,camel_case_types,unnecessary_const,non_constant_identifier_names,library_prefixes,unused_import,unused_shown_name,return_of_invalid_type,unnecessary_this,prefer_final_fields
diff --git a/grpc/example/googleapis/lib/src/generated/google/logging/type/http_request.pbjson.dart b/grpc/example/googleapis/lib/src/generated/google/logging/type/http_request.pbjson.dart new file mode 100644 index 0000000..f625cb8 --- /dev/null +++ b/grpc/example/googleapis/lib/src/generated/google/logging/type/http_request.pbjson.dart
@@ -0,0 +1,52 @@ +/// +// Generated code. Do not modify. +// source: google/logging/type/http_request.proto +// +// @dart = 2.7 +// ignore_for_file: annotate_overrides,camel_case_types,unnecessary_const,non_constant_identifier_names,library_prefixes,unused_import,unused_shown_name,return_of_invalid_type,unnecessary_this,prefer_final_fields + +const HttpRequest$json = const { + '1': 'HttpRequest', + '2': const [ + const { + '1': 'request_method', + '3': 1, + '4': 1, + '5': 9, + '10': 'requestMethod' + }, + const {'1': 'request_url', '3': 2, '4': 1, '5': 9, '10': 'requestUrl'}, + const {'1': 'request_size', '3': 3, '4': 1, '5': 3, '10': 'requestSize'}, + const {'1': 'status', '3': 4, '4': 1, '5': 5, '10': 'status'}, + const {'1': 'response_size', '3': 5, '4': 1, '5': 3, '10': 'responseSize'}, + const {'1': 'user_agent', '3': 6, '4': 1, '5': 9, '10': 'userAgent'}, + const {'1': 'remote_ip', '3': 7, '4': 1, '5': 9, '10': 'remoteIp'}, + const {'1': 'server_ip', '3': 13, '4': 1, '5': 9, '10': 'serverIp'}, + const {'1': 'referer', '3': 8, '4': 1, '5': 9, '10': 'referer'}, + const { + '1': 'latency', + '3': 14, + '4': 1, + '5': 11, + '6': '.google.protobuf.Duration', + '10': 'latency' + }, + const {'1': 'cache_lookup', '3': 11, '4': 1, '5': 8, '10': 'cacheLookup'}, + const {'1': 'cache_hit', '3': 9, '4': 1, '5': 8, '10': 'cacheHit'}, + const { + '1': 'cache_validated_with_origin_server', + '3': 10, + '4': 1, + '5': 8, + '10': 'cacheValidatedWithOriginServer' + }, + const { + '1': 'cache_fill_bytes', + '3': 12, + '4': 1, + '5': 3, + '10': 'cacheFillBytes' + }, + const {'1': 'protocol', '3': 15, '4': 1, '5': 9, '10': 'protocol'}, + ], +};
diff --git a/grpc/example/googleapis/lib/src/generated/google/logging/type/log_severity.pb.dart b/grpc/example/googleapis/lib/src/generated/google/logging/type/log_severity.pb.dart new file mode 100644 index 0000000..68e8488 --- /dev/null +++ b/grpc/example/googleapis/lib/src/generated/google/logging/type/log_severity.pb.dart
@@ -0,0 +1,10 @@ +/// +// Generated code. Do not modify. +// source: google/logging/type/log_severity.proto +// +// @dart = 2.7 +// ignore_for_file: annotate_overrides,camel_case_types,unnecessary_const,non_constant_identifier_names,library_prefixes,unused_import,unused_shown_name,return_of_invalid_type,unnecessary_this,prefer_final_fields + +import 'dart:core' as $core; + +export 'log_severity.pbenum.dart';
diff --git a/grpc/example/googleapis/lib/src/generated/google/logging/type/log_severity.pbenum.dart b/grpc/example/googleapis/lib/src/generated/google/logging/type/log_severity.pbenum.dart new file mode 100644 index 0000000..f63f7cd --- /dev/null +++ b/grpc/example/googleapis/lib/src/generated/google/logging/type/log_severity.pbenum.dart
@@ -0,0 +1,76 @@ +/// +// Generated code. Do not modify. +// source: google/logging/type/log_severity.proto +// +// @dart = 2.7 +// ignore_for_file: annotate_overrides,camel_case_types,unnecessary_const,non_constant_identifier_names,library_prefixes,unused_import,unused_shown_name,return_of_invalid_type,unnecessary_this,prefer_final_fields + +// ignore_for_file: UNDEFINED_SHOWN_NAME +import 'dart:core' as $core; +import 'package:protobuf/protobuf.dart' as $pb; + +class LogSeverity extends $pb.ProtobufEnum { + static const LogSeverity DEFAULT = LogSeverity._( + 0, + const $core.bool.fromEnvironment('protobuf.omit_enum_names') + ? '' + : 'DEFAULT'); + static const LogSeverity DEBUG = LogSeverity._( + 100, + const $core.bool.fromEnvironment('protobuf.omit_enum_names') + ? '' + : 'DEBUG'); + static const LogSeverity INFO = LogSeverity._( + 200, + const $core.bool.fromEnvironment('protobuf.omit_enum_names') + ? '' + : 'INFO'); + static const LogSeverity NOTICE = LogSeverity._( + 300, + const $core.bool.fromEnvironment('protobuf.omit_enum_names') + ? '' + : 'NOTICE'); + static const LogSeverity WARNING = LogSeverity._( + 400, + const $core.bool.fromEnvironment('protobuf.omit_enum_names') + ? '' + : 'WARNING'); + static const LogSeverity ERROR = LogSeverity._( + 500, + const $core.bool.fromEnvironment('protobuf.omit_enum_names') + ? '' + : 'ERROR'); + static const LogSeverity CRITICAL = LogSeverity._( + 600, + const $core.bool.fromEnvironment('protobuf.omit_enum_names') + ? '' + : 'CRITICAL'); + static const LogSeverity ALERT = LogSeverity._( + 700, + const $core.bool.fromEnvironment('protobuf.omit_enum_names') + ? '' + : 'ALERT'); + static const LogSeverity EMERGENCY = LogSeverity._( + 800, + const $core.bool.fromEnvironment('protobuf.omit_enum_names') + ? '' + : 'EMERGENCY'); + + static const $core.List<LogSeverity> values = <LogSeverity>[ + DEFAULT, + DEBUG, + INFO, + NOTICE, + WARNING, + ERROR, + CRITICAL, + ALERT, + EMERGENCY, + ]; + + static final $core.Map<$core.int, LogSeverity> _byValue = + $pb.ProtobufEnum.initByValue(values); + static LogSeverity valueOf($core.int value) => _byValue[value]; + + const LogSeverity._($core.int v, $core.String n) : super(v, n); +}
diff --git a/grpc/example/googleapis/lib/src/generated/google/logging/type/log_severity.pbjson.dart b/grpc/example/googleapis/lib/src/generated/google/logging/type/log_severity.pbjson.dart new file mode 100644 index 0000000..480f2e0 --- /dev/null +++ b/grpc/example/googleapis/lib/src/generated/google/logging/type/log_severity.pbjson.dart
@@ -0,0 +1,21 @@ +/// +// Generated code. Do not modify. +// source: google/logging/type/log_severity.proto +// +// @dart = 2.7 +// ignore_for_file: annotate_overrides,camel_case_types,unnecessary_const,non_constant_identifier_names,library_prefixes,unused_import,unused_shown_name,return_of_invalid_type,unnecessary_this,prefer_final_fields + +const LogSeverity$json = const { + '1': 'LogSeverity', + '2': const [ + const {'1': 'DEFAULT', '2': 0}, + const {'1': 'DEBUG', '2': 100}, + const {'1': 'INFO', '2': 200}, + const {'1': 'NOTICE', '2': 300}, + const {'1': 'WARNING', '2': 400}, + const {'1': 'ERROR', '2': 500}, + const {'1': 'CRITICAL', '2': 600}, + const {'1': 'ALERT', '2': 700}, + const {'1': 'EMERGENCY', '2': 800}, + ], +};
diff --git a/grpc/example/googleapis/lib/src/generated/google/logging/v2/log_entry.pb.dart b/grpc/example/googleapis/lib/src/generated/google/logging/v2/log_entry.pb.dart new file mode 100644 index 0000000..136fcdf --- /dev/null +++ b/grpc/example/googleapis/lib/src/generated/google/logging/v2/log_entry.pb.dart
@@ -0,0 +1,676 @@ +/// +// Generated code. Do not modify. +// source: google/logging/v2/log_entry.proto +// +// @dart = 2.7 +// ignore_for_file: annotate_overrides,camel_case_types,unnecessary_const,non_constant_identifier_names,library_prefixes,unused_import,unused_shown_name,return_of_invalid_type,unnecessary_this,prefer_final_fields + +import 'dart:core' as $core; + +import 'package:fixnum/fixnum.dart' as $fixnum; +import 'package:protobuf/protobuf.dart' as $pb; + +import '../../protobuf/any.pb.dart' as $0; +import '../../protobuf/struct.pb.dart' as $1; +import '../type/http_request.pb.dart' as $2; +import '../../api/monitored_resource.pb.dart' as $3; +import '../../protobuf/timestamp.pb.dart' as $4; + +import '../type/log_severity.pbenum.dart' as $5; + +enum LogEntry_Payload { protoPayload, textPayload, jsonPayload, notSet } + +class LogEntry extends $pb.GeneratedMessage { + static const $core.Map<$core.int, LogEntry_Payload> _LogEntry_PayloadByTag = { + 2: LogEntry_Payload.protoPayload, + 3: LogEntry_Payload.textPayload, + 6: LogEntry_Payload.jsonPayload, + 0: LogEntry_Payload.notSet + }; + static final $pb.BuilderInfo _i = $pb.BuilderInfo( + const $core.bool.fromEnvironment('protobuf.omit_message_names') + ? '' + : 'LogEntry', + package: const $pb.PackageName( + const $core.bool.fromEnvironment('protobuf.omit_message_names') + ? '' + : 'google.logging.v2'), + createEmptyInstance: create) + ..oo(0, [2, 3, 6]) + ..aOM<$0.Any>( + 2, + const $core.bool.fromEnvironment('protobuf.omit_field_names') + ? '' + : 'protoPayload', + subBuilder: $0.Any.create) + ..aOS( + 3, + const $core.bool.fromEnvironment('protobuf.omit_field_names') + ? '' + : 'textPayload') + ..aOS( + 4, + const $core.bool.fromEnvironment('protobuf.omit_field_names') + ? '' + : 'insertId') + ..aOM<$1.Struct>( + 6, + const $core.bool.fromEnvironment('protobuf.omit_field_names') + ? '' + : 'jsonPayload', + subBuilder: $1.Struct.create) + ..aOM<$2.HttpRequest>( + 7, + const $core.bool.fromEnvironment('protobuf.omit_field_names') + ? '' + : 'httpRequest', + subBuilder: $2.HttpRequest.create) + ..aOM<$3.MonitoredResource>( + 8, + const $core.bool.fromEnvironment('protobuf.omit_field_names') + ? '' + : 'resource', + subBuilder: $3.MonitoredResource.create) + ..aOM<$4.Timestamp>( + 9, + const $core.bool.fromEnvironment('protobuf.omit_field_names') + ? '' + : 'timestamp', + subBuilder: $4.Timestamp.create) + ..e<$5.LogSeverity>( + 10, + const $core.bool.fromEnvironment('protobuf.omit_field_names') + ? '' + : 'severity', + $pb.PbFieldType.OE, + defaultOrMaker: $5.LogSeverity.DEFAULT, + valueOf: $5.LogSeverity.valueOf, + enumValues: $5.LogSeverity.values) + ..m<$core.String, $core.String>( + 11, + const $core.bool.fromEnvironment('protobuf.omit_field_names') + ? '' + : 'labels', + entryClassName: 'LogEntry.LabelsEntry', + keyFieldType: $pb.PbFieldType.OS, + valueFieldType: $pb.PbFieldType.OS, + packageName: const $pb.PackageName('google.logging.v2')) + ..aOS( + 12, + const $core.bool.fromEnvironment('protobuf.omit_field_names') + ? '' + : 'logName') + ..aOM<LogEntryOperation>( + 15, + const $core.bool.fromEnvironment('protobuf.omit_field_names') + ? '' + : 'operation', + subBuilder: LogEntryOperation.create) + ..aOS( + 22, + const $core.bool.fromEnvironment('protobuf.omit_field_names') + ? '' + : 'trace') + ..aOM<LogEntrySourceLocation>( + 23, + const $core.bool.fromEnvironment('protobuf.omit_field_names') + ? '' + : 'sourceLocation', + subBuilder: LogEntrySourceLocation.create) + ..aOM<$4.Timestamp>( + 24, + const $core.bool.fromEnvironment('protobuf.omit_field_names') + ? '' + : 'receiveTimestamp', + subBuilder: $4.Timestamp.create) + ..aOS( + 27, + const $core.bool.fromEnvironment('protobuf.omit_field_names') + ? '' + : 'spanId') + ..aOB( + 30, + const $core.bool.fromEnvironment('protobuf.omit_field_names') + ? '' + : 'traceSampled') + ..hasRequiredFields = false; + + LogEntry._() : super(); + factory LogEntry({ + $0.Any protoPayload, + $core.String textPayload, + $core.String insertId, + $1.Struct jsonPayload, + $2.HttpRequest httpRequest, + $3.MonitoredResource resource, + $4.Timestamp timestamp, + $5.LogSeverity severity, + $core.Map<$core.String, $core.String> labels, + $core.String logName, + LogEntryOperation operation, + $core.String trace, + LogEntrySourceLocation sourceLocation, + $4.Timestamp receiveTimestamp, + $core.String spanId, + $core.bool traceSampled, + }) { + final _result = create(); + if (protoPayload != null) { + _result.protoPayload = protoPayload; + } + if (textPayload != null) { + _result.textPayload = textPayload; + } + if (insertId != null) { + _result.insertId = insertId; + } + if (jsonPayload != null) { + _result.jsonPayload = jsonPayload; + } + if (httpRequest != null) { + _result.httpRequest = httpRequest; + } + if (resource != null) { + _result.resource = resource; + } + if (timestamp != null) { + _result.timestamp = timestamp; + } + if (severity != null) { + _result.severity = severity; + } + if (labels != null) { + _result.labels.addAll(labels); + } + if (logName != null) { + _result.logName = logName; + } + if (operation != null) { + _result.operation = operation; + } + if (trace != null) { + _result.trace = trace; + } + if (sourceLocation != null) { + _result.sourceLocation = sourceLocation; + } + if (receiveTimestamp != null) { + _result.receiveTimestamp = receiveTimestamp; + } + if (spanId != null) { + _result.spanId = spanId; + } + if (traceSampled != null) { + _result.traceSampled = traceSampled; + } + return _result; + } + factory LogEntry.fromBuffer($core.List<$core.int> i, + [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromBuffer(i, r); + factory LogEntry.fromJson($core.String i, + [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromJson(i, r); + @$core.Deprecated('Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.deepCopy] instead. ' + 'Will be removed in next major version') + LogEntry clone() => LogEntry()..mergeFromMessage(this); + @$core.Deprecated('Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.rebuild] instead. ' + 'Will be removed in next major version') + LogEntry copyWith(void Function(LogEntry) updates) => + super.copyWith((message) => + updates(message as LogEntry)); // ignore: deprecated_member_use + $pb.BuilderInfo get info_ => _i; + @$core.pragma('dart2js:noInline') + static LogEntry create() => LogEntry._(); + LogEntry createEmptyInstance() => create(); + static $pb.PbList<LogEntry> createRepeated() => $pb.PbList<LogEntry>(); + @$core.pragma('dart2js:noInline') + static LogEntry getDefault() => + _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor<LogEntry>(create); + static LogEntry _defaultInstance; + + LogEntry_Payload whichPayload() => _LogEntry_PayloadByTag[$_whichOneof(0)]; + void clearPayload() => clearField($_whichOneof(0)); + + @$pb.TagNumber(2) + $0.Any get protoPayload => $_getN(0); + @$pb.TagNumber(2) + set protoPayload($0.Any v) { + setField(2, v); + } + + @$pb.TagNumber(2) + $core.bool hasProtoPayload() => $_has(0); + @$pb.TagNumber(2) + void clearProtoPayload() => clearField(2); + @$pb.TagNumber(2) + $0.Any ensureProtoPayload() => $_ensure(0); + + @$pb.TagNumber(3) + $core.String get textPayload => $_getSZ(1); + @$pb.TagNumber(3) + set textPayload($core.String v) { + $_setString(1, v); + } + + @$pb.TagNumber(3) + $core.bool hasTextPayload() => $_has(1); + @$pb.TagNumber(3) + void clearTextPayload() => clearField(3); + + @$pb.TagNumber(4) + $core.String get insertId => $_getSZ(2); + @$pb.TagNumber(4) + set insertId($core.String v) { + $_setString(2, v); + } + + @$pb.TagNumber(4) + $core.bool hasInsertId() => $_has(2); + @$pb.TagNumber(4) + void clearInsertId() => clearField(4); + + @$pb.TagNumber(6) + $1.Struct get jsonPayload => $_getN(3); + @$pb.TagNumber(6) + set jsonPayload($1.Struct v) { + setField(6, v); + } + + @$pb.TagNumber(6) + $core.bool hasJsonPayload() => $_has(3); + @$pb.TagNumber(6) + void clearJsonPayload() => clearField(6); + @$pb.TagNumber(6) + $1.Struct ensureJsonPayload() => $_ensure(3); + + @$pb.TagNumber(7) + $2.HttpRequest get httpRequest => $_getN(4); + @$pb.TagNumber(7) + set httpRequest($2.HttpRequest v) { + setField(7, v); + } + + @$pb.TagNumber(7) + $core.bool hasHttpRequest() => $_has(4); + @$pb.TagNumber(7) + void clearHttpRequest() => clearField(7); + @$pb.TagNumber(7) + $2.HttpRequest ensureHttpRequest() => $_ensure(4); + + @$pb.TagNumber(8) + $3.MonitoredResource get resource => $_getN(5); + @$pb.TagNumber(8) + set resource($3.MonitoredResource v) { + setField(8, v); + } + + @$pb.TagNumber(8) + $core.bool hasResource() => $_has(5); + @$pb.TagNumber(8) + void clearResource() => clearField(8); + @$pb.TagNumber(8) + $3.MonitoredResource ensureResource() => $_ensure(5); + + @$pb.TagNumber(9) + $4.Timestamp get timestamp => $_getN(6); + @$pb.TagNumber(9) + set timestamp($4.Timestamp v) { + setField(9, v); + } + + @$pb.TagNumber(9) + $core.bool hasTimestamp() => $_has(6); + @$pb.TagNumber(9) + void clearTimestamp() => clearField(9); + @$pb.TagNumber(9) + $4.Timestamp ensureTimestamp() => $_ensure(6); + + @$pb.TagNumber(10) + $5.LogSeverity get severity => $_getN(7); + @$pb.TagNumber(10) + set severity($5.LogSeverity v) { + setField(10, v); + } + + @$pb.TagNumber(10) + $core.bool hasSeverity() => $_has(7); + @$pb.TagNumber(10) + void clearSeverity() => clearField(10); + + @$pb.TagNumber(11) + $core.Map<$core.String, $core.String> get labels => $_getMap(8); + + @$pb.TagNumber(12) + $core.String get logName => $_getSZ(9); + @$pb.TagNumber(12) + set logName($core.String v) { + $_setString(9, v); + } + + @$pb.TagNumber(12) + $core.bool hasLogName() => $_has(9); + @$pb.TagNumber(12) + void clearLogName() => clearField(12); + + @$pb.TagNumber(15) + LogEntryOperation get operation => $_getN(10); + @$pb.TagNumber(15) + set operation(LogEntryOperation v) { + setField(15, v); + } + + @$pb.TagNumber(15) + $core.bool hasOperation() => $_has(10); + @$pb.TagNumber(15) + void clearOperation() => clearField(15); + @$pb.TagNumber(15) + LogEntryOperation ensureOperation() => $_ensure(10); + + @$pb.TagNumber(22) + $core.String get trace => $_getSZ(11); + @$pb.TagNumber(22) + set trace($core.String v) { + $_setString(11, v); + } + + @$pb.TagNumber(22) + $core.bool hasTrace() => $_has(11); + @$pb.TagNumber(22) + void clearTrace() => clearField(22); + + @$pb.TagNumber(23) + LogEntrySourceLocation get sourceLocation => $_getN(12); + @$pb.TagNumber(23) + set sourceLocation(LogEntrySourceLocation v) { + setField(23, v); + } + + @$pb.TagNumber(23) + $core.bool hasSourceLocation() => $_has(12); + @$pb.TagNumber(23) + void clearSourceLocation() => clearField(23); + @$pb.TagNumber(23) + LogEntrySourceLocation ensureSourceLocation() => $_ensure(12); + + @$pb.TagNumber(24) + $4.Timestamp get receiveTimestamp => $_getN(13); + @$pb.TagNumber(24) + set receiveTimestamp($4.Timestamp v) { + setField(24, v); + } + + @$pb.TagNumber(24) + $core.bool hasReceiveTimestamp() => $_has(13); + @$pb.TagNumber(24) + void clearReceiveTimestamp() => clearField(24); + @$pb.TagNumber(24) + $4.Timestamp ensureReceiveTimestamp() => $_ensure(13); + + @$pb.TagNumber(27) + $core.String get spanId => $_getSZ(14); + @$pb.TagNumber(27) + set spanId($core.String v) { + $_setString(14, v); + } + + @$pb.TagNumber(27) + $core.bool hasSpanId() => $_has(14); + @$pb.TagNumber(27) + void clearSpanId() => clearField(27); + + @$pb.TagNumber(30) + $core.bool get traceSampled => $_getBF(15); + @$pb.TagNumber(30) + set traceSampled($core.bool v) { + $_setBool(15, v); + } + + @$pb.TagNumber(30) + $core.bool hasTraceSampled() => $_has(15); + @$pb.TagNumber(30) + void clearTraceSampled() => clearField(30); +} + +class LogEntryOperation extends $pb.GeneratedMessage { + static final $pb.BuilderInfo _i = $pb.BuilderInfo( + const $core.bool.fromEnvironment('protobuf.omit_message_names') + ? '' + : 'LogEntryOperation', + package: const $pb.PackageName( + const $core.bool.fromEnvironment('protobuf.omit_message_names') + ? '' + : 'google.logging.v2'), + createEmptyInstance: create) + ..aOS( + 1, + const $core.bool.fromEnvironment('protobuf.omit_field_names') + ? '' + : 'id') + ..aOS( + 2, + const $core.bool.fromEnvironment('protobuf.omit_field_names') + ? '' + : 'producer') + ..aOB( + 3, + const $core.bool.fromEnvironment('protobuf.omit_field_names') + ? '' + : 'first') + ..aOB( + 4, + const $core.bool.fromEnvironment('protobuf.omit_field_names') + ? '' + : 'last') + ..hasRequiredFields = false; + + LogEntryOperation._() : super(); + factory LogEntryOperation({ + $core.String id, + $core.String producer, + $core.bool first, + $core.bool last, + }) { + final _result = create(); + if (id != null) { + _result.id = id; + } + if (producer != null) { + _result.producer = producer; + } + if (first != null) { + _result.first = first; + } + if (last != null) { + _result.last = last; + } + return _result; + } + factory LogEntryOperation.fromBuffer($core.List<$core.int> i, + [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromBuffer(i, r); + factory LogEntryOperation.fromJson($core.String i, + [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromJson(i, r); + @$core.Deprecated('Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.deepCopy] instead. ' + 'Will be removed in next major version') + LogEntryOperation clone() => LogEntryOperation()..mergeFromMessage(this); + @$core.Deprecated('Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.rebuild] instead. ' + 'Will be removed in next major version') + LogEntryOperation copyWith(void Function(LogEntryOperation) updates) => + super.copyWith((message) => updates( + message as LogEntryOperation)); // ignore: deprecated_member_use + $pb.BuilderInfo get info_ => _i; + @$core.pragma('dart2js:noInline') + static LogEntryOperation create() => LogEntryOperation._(); + LogEntryOperation createEmptyInstance() => create(); + static $pb.PbList<LogEntryOperation> createRepeated() => + $pb.PbList<LogEntryOperation>(); + @$core.pragma('dart2js:noInline') + static LogEntryOperation getDefault() => _defaultInstance ??= + $pb.GeneratedMessage.$_defaultFor<LogEntryOperation>(create); + static LogEntryOperation _defaultInstance; + + @$pb.TagNumber(1) + $core.String get id => $_getSZ(0); + @$pb.TagNumber(1) + set id($core.String v) { + $_setString(0, v); + } + + @$pb.TagNumber(1) + $core.bool hasId() => $_has(0); + @$pb.TagNumber(1) + void clearId() => clearField(1); + + @$pb.TagNumber(2) + $core.String get producer => $_getSZ(1); + @$pb.TagNumber(2) + set producer($core.String v) { + $_setString(1, v); + } + + @$pb.TagNumber(2) + $core.bool hasProducer() => $_has(1); + @$pb.TagNumber(2) + void clearProducer() => clearField(2); + + @$pb.TagNumber(3) + $core.bool get first => $_getBF(2); + @$pb.TagNumber(3) + set first($core.bool v) { + $_setBool(2, v); + } + + @$pb.TagNumber(3) + $core.bool hasFirst() => $_has(2); + @$pb.TagNumber(3) + void clearFirst() => clearField(3); + + @$pb.TagNumber(4) + $core.bool get last => $_getBF(3); + @$pb.TagNumber(4) + set last($core.bool v) { + $_setBool(3, v); + } + + @$pb.TagNumber(4) + $core.bool hasLast() => $_has(3); + @$pb.TagNumber(4) + void clearLast() => clearField(4); +} + +class LogEntrySourceLocation extends $pb.GeneratedMessage { + static final $pb.BuilderInfo _i = $pb.BuilderInfo( + const $core.bool.fromEnvironment('protobuf.omit_message_names') + ? '' + : 'LogEntrySourceLocation', + package: const $pb.PackageName( + const $core.bool.fromEnvironment('protobuf.omit_message_names') + ? '' + : 'google.logging.v2'), + createEmptyInstance: create) + ..aOS( + 1, + const $core.bool.fromEnvironment('protobuf.omit_field_names') + ? '' + : 'file') + ..aInt64( + 2, + const $core.bool.fromEnvironment('protobuf.omit_field_names') + ? '' + : 'line') + ..aOS( + 3, + const $core.bool.fromEnvironment('protobuf.omit_field_names') + ? '' + : 'function') + ..hasRequiredFields = false; + + LogEntrySourceLocation._() : super(); + factory LogEntrySourceLocation({ + $core.String file, + $fixnum.Int64 line, + $core.String function, + }) { + final _result = create(); + if (file != null) { + _result.file = file; + } + if (line != null) { + _result.line = line; + } + if (function != null) { + _result.function = function; + } + return _result; + } + factory LogEntrySourceLocation.fromBuffer($core.List<$core.int> i, + [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromBuffer(i, r); + factory LogEntrySourceLocation.fromJson($core.String i, + [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromJson(i, r); + @$core.Deprecated('Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.deepCopy] instead. ' + 'Will be removed in next major version') + LogEntrySourceLocation clone() => + LogEntrySourceLocation()..mergeFromMessage(this); + @$core.Deprecated('Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.rebuild] instead. ' + 'Will be removed in next major version') + LogEntrySourceLocation copyWith( + void Function(LogEntrySourceLocation) updates) => + super.copyWith((message) => updates( + message as LogEntrySourceLocation)); // ignore: deprecated_member_use + $pb.BuilderInfo get info_ => _i; + @$core.pragma('dart2js:noInline') + static LogEntrySourceLocation create() => LogEntrySourceLocation._(); + LogEntrySourceLocation createEmptyInstance() => create(); + static $pb.PbList<LogEntrySourceLocation> createRepeated() => + $pb.PbList<LogEntrySourceLocation>(); + @$core.pragma('dart2js:noInline') + static LogEntrySourceLocation getDefault() => _defaultInstance ??= + $pb.GeneratedMessage.$_defaultFor<LogEntrySourceLocation>(create); + static LogEntrySourceLocation _defaultInstance; + + @$pb.TagNumber(1) + $core.String get file => $_getSZ(0); + @$pb.TagNumber(1) + set file($core.String v) { + $_setString(0, v); + } + + @$pb.TagNumber(1) + $core.bool hasFile() => $_has(0); + @$pb.TagNumber(1) + void clearFile() => clearField(1); + + @$pb.TagNumber(2) + $fixnum.Int64 get line => $_getI64(1); + @$pb.TagNumber(2) + set line($fixnum.Int64 v) { + $_setInt64(1, v); + } + + @$pb.TagNumber(2) + $core.bool hasLine() => $_has(1); + @$pb.TagNumber(2) + void clearLine() => clearField(2); + + @$pb.TagNumber(3) + $core.String get function => $_getSZ(2); + @$pb.TagNumber(3) + set function($core.String v) { + $_setString(2, v); + } + + @$pb.TagNumber(3) + $core.bool hasFunction() => $_has(2); + @$pb.TagNumber(3) + void clearFunction() => clearField(3); +}
diff --git a/grpc/example/googleapis/lib/src/generated/google/logging/v2/log_entry.pbenum.dart b/grpc/example/googleapis/lib/src/generated/google/logging/v2/log_entry.pbenum.dart new file mode 100644 index 0000000..26475f4 --- /dev/null +++ b/grpc/example/googleapis/lib/src/generated/google/logging/v2/log_entry.pbenum.dart
@@ -0,0 +1,6 @@ +/// +// Generated code. Do not modify. +// source: google/logging/v2/log_entry.proto +// +// @dart = 2.7 +// ignore_for_file: annotate_overrides,camel_case_types,unnecessary_const,non_constant_identifier_names,library_prefixes,unused_import,unused_shown_name,return_of_invalid_type,unnecessary_this,prefer_final_fields
diff --git a/grpc/example/googleapis/lib/src/generated/google/logging/v2/log_entry.pbjson.dart b/grpc/example/googleapis/lib/src/generated/google/logging/v2/log_entry.pbjson.dart new file mode 100644 index 0000000..53b6376 --- /dev/null +++ b/grpc/example/googleapis/lib/src/generated/google/logging/v2/log_entry.pbjson.dart
@@ -0,0 +1,190 @@ +/// +// Generated code. Do not modify. +// source: google/logging/v2/log_entry.proto +// +// @dart = 2.7 +// ignore_for_file: annotate_overrides,camel_case_types,unnecessary_const,non_constant_identifier_names,library_prefixes,unused_import,unused_shown_name,return_of_invalid_type,unnecessary_this,prefer_final_fields + +const LogEntry$json = const { + '1': 'LogEntry', + '2': const [ + const { + '1': 'log_name', + '3': 12, + '4': 1, + '5': 9, + '8': const {}, + '10': 'logName' + }, + const { + '1': 'resource', + '3': 8, + '4': 1, + '5': 11, + '6': '.google.api.MonitoredResource', + '8': const {}, + '10': 'resource' + }, + const { + '1': 'proto_payload', + '3': 2, + '4': 1, + '5': 11, + '6': '.google.protobuf.Any', + '9': 0, + '10': 'protoPayload' + }, + const { + '1': 'text_payload', + '3': 3, + '4': 1, + '5': 9, + '9': 0, + '10': 'textPayload' + }, + const { + '1': 'json_payload', + '3': 6, + '4': 1, + '5': 11, + '6': '.google.protobuf.Struct', + '9': 0, + '10': 'jsonPayload' + }, + const { + '1': 'timestamp', + '3': 9, + '4': 1, + '5': 11, + '6': '.google.protobuf.Timestamp', + '8': const {}, + '10': 'timestamp' + }, + const { + '1': 'receive_timestamp', + '3': 24, + '4': 1, + '5': 11, + '6': '.google.protobuf.Timestamp', + '8': const {}, + '10': 'receiveTimestamp' + }, + const { + '1': 'severity', + '3': 10, + '4': 1, + '5': 14, + '6': '.google.logging.type.LogSeverity', + '8': const {}, + '10': 'severity' + }, + const { + '1': 'insert_id', + '3': 4, + '4': 1, + '5': 9, + '8': const {}, + '10': 'insertId' + }, + const { + '1': 'http_request', + '3': 7, + '4': 1, + '5': 11, + '6': '.google.logging.type.HttpRequest', + '8': const {}, + '10': 'httpRequest' + }, + const { + '1': 'labels', + '3': 11, + '4': 3, + '5': 11, + '6': '.google.logging.v2.LogEntry.LabelsEntry', + '8': const {}, + '10': 'labels' + }, + const { + '1': 'operation', + '3': 15, + '4': 1, + '5': 11, + '6': '.google.logging.v2.LogEntryOperation', + '8': const {}, + '10': 'operation' + }, + const {'1': 'trace', '3': 22, '4': 1, '5': 9, '8': const {}, '10': 'trace'}, + const { + '1': 'span_id', + '3': 27, + '4': 1, + '5': 9, + '8': const {}, + '10': 'spanId' + }, + const { + '1': 'trace_sampled', + '3': 30, + '4': 1, + '5': 8, + '8': const {}, + '10': 'traceSampled' + }, + const { + '1': 'source_location', + '3': 23, + '4': 1, + '5': 11, + '6': '.google.logging.v2.LogEntrySourceLocation', + '8': const {}, + '10': 'sourceLocation' + }, + ], + '3': const [LogEntry_LabelsEntry$json], + '7': const {}, + '8': const [ + const {'1': 'payload'}, + ], +}; + +const LogEntry_LabelsEntry$json = const { + '1': 'LabelsEntry', + '2': const [ + const {'1': 'key', '3': 1, '4': 1, '5': 9, '10': 'key'}, + const {'1': 'value', '3': 2, '4': 1, '5': 9, '10': 'value'}, + ], + '7': const {'7': true}, +}; + +const LogEntryOperation$json = const { + '1': 'LogEntryOperation', + '2': const [ + const {'1': 'id', '3': 1, '4': 1, '5': 9, '8': const {}, '10': 'id'}, + const { + '1': 'producer', + '3': 2, + '4': 1, + '5': 9, + '8': const {}, + '10': 'producer' + }, + const {'1': 'first', '3': 3, '4': 1, '5': 8, '8': const {}, '10': 'first'}, + const {'1': 'last', '3': 4, '4': 1, '5': 8, '8': const {}, '10': 'last'}, + ], +}; + +const LogEntrySourceLocation$json = const { + '1': 'LogEntrySourceLocation', + '2': const [ + const {'1': 'file', '3': 1, '4': 1, '5': 9, '8': const {}, '10': 'file'}, + const {'1': 'line', '3': 2, '4': 1, '5': 3, '8': const {}, '10': 'line'}, + const { + '1': 'function', + '3': 3, + '4': 1, + '5': 9, + '8': const {}, + '10': 'function' + }, + ], +};
diff --git a/grpc/example/googleapis/lib/src/generated/google/logging/v2/logging.pb.dart b/grpc/example/googleapis/lib/src/generated/google/logging/v2/logging.pb.dart new file mode 100644 index 0000000..939933d --- /dev/null +++ b/grpc/example/googleapis/lib/src/generated/google/logging/v2/logging.pb.dart
@@ -0,0 +1,1253 @@ +/// +// Generated code. Do not modify. +// source: google/logging/v2/logging.proto +// +// @dart = 2.7 +// ignore_for_file: annotate_overrides,camel_case_types,unnecessary_const,non_constant_identifier_names,library_prefixes,unused_import,unused_shown_name,return_of_invalid_type,unnecessary_this,prefer_final_fields + +import 'dart:core' as $core; + +import 'package:protobuf/protobuf.dart' as $pb; + +import '../../api/monitored_resource.pb.dart' as $3; +import 'log_entry.pb.dart' as $4; +import '../../rpc/status.pb.dart' as $5; +import '../../protobuf/duration.pb.dart' as $6; + +import 'logging.pbenum.dart'; + +export 'logging.pbenum.dart'; + +class DeleteLogRequest extends $pb.GeneratedMessage { + static final $pb.BuilderInfo _i = $pb.BuilderInfo( + const $core.bool.fromEnvironment('protobuf.omit_message_names') + ? '' + : 'DeleteLogRequest', + package: const $pb.PackageName( + const $core.bool.fromEnvironment('protobuf.omit_message_names') + ? '' + : 'google.logging.v2'), + createEmptyInstance: create) + ..aOS( + 1, + const $core.bool.fromEnvironment('protobuf.omit_field_names') + ? '' + : 'logName') + ..hasRequiredFields = false; + + DeleteLogRequest._() : super(); + factory DeleteLogRequest({ + $core.String logName, + }) { + final _result = create(); + if (logName != null) { + _result.logName = logName; + } + return _result; + } + factory DeleteLogRequest.fromBuffer($core.List<$core.int> i, + [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromBuffer(i, r); + factory DeleteLogRequest.fromJson($core.String i, + [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromJson(i, r); + @$core.Deprecated('Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.deepCopy] instead. ' + 'Will be removed in next major version') + DeleteLogRequest clone() => DeleteLogRequest()..mergeFromMessage(this); + @$core.Deprecated('Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.rebuild] instead. ' + 'Will be removed in next major version') + DeleteLogRequest copyWith(void Function(DeleteLogRequest) updates) => + super.copyWith((message) => updates( + message as DeleteLogRequest)); // ignore: deprecated_member_use + $pb.BuilderInfo get info_ => _i; + @$core.pragma('dart2js:noInline') + static DeleteLogRequest create() => DeleteLogRequest._(); + DeleteLogRequest createEmptyInstance() => create(); + static $pb.PbList<DeleteLogRequest> createRepeated() => + $pb.PbList<DeleteLogRequest>(); + @$core.pragma('dart2js:noInline') + static DeleteLogRequest getDefault() => _defaultInstance ??= + $pb.GeneratedMessage.$_defaultFor<DeleteLogRequest>(create); + static DeleteLogRequest _defaultInstance; + + @$pb.TagNumber(1) + $core.String get logName => $_getSZ(0); + @$pb.TagNumber(1) + set logName($core.String v) { + $_setString(0, v); + } + + @$pb.TagNumber(1) + $core.bool hasLogName() => $_has(0); + @$pb.TagNumber(1) + void clearLogName() => clearField(1); +} + +class WriteLogEntriesRequest extends $pb.GeneratedMessage { + static final $pb.BuilderInfo _i = $pb.BuilderInfo( + const $core.bool.fromEnvironment('protobuf.omit_message_names') + ? '' + : 'WriteLogEntriesRequest', + package: const $pb.PackageName( + const $core.bool.fromEnvironment('protobuf.omit_message_names') + ? '' + : 'google.logging.v2'), + createEmptyInstance: create) + ..aOS( + 1, + const $core.bool.fromEnvironment('protobuf.omit_field_names') + ? '' + : 'logName') + ..aOM<$3.MonitoredResource>( + 2, + const $core.bool.fromEnvironment('protobuf.omit_field_names') + ? '' + : 'resource', + subBuilder: $3.MonitoredResource.create) + ..m<$core.String, $core.String>( + 3, + const $core.bool.fromEnvironment('protobuf.omit_field_names') + ? '' + : 'labels', + entryClassName: 'WriteLogEntriesRequest.LabelsEntry', + keyFieldType: $pb.PbFieldType.OS, + valueFieldType: $pb.PbFieldType.OS, + packageName: const $pb.PackageName('google.logging.v2')) + ..pc<$4.LogEntry>( + 4, + const $core.bool.fromEnvironment('protobuf.omit_field_names') + ? '' + : 'entries', + $pb.PbFieldType.PM, + subBuilder: $4.LogEntry.create) + ..aOB( + 5, + const $core.bool.fromEnvironment('protobuf.omit_field_names') + ? '' + : 'partialSuccess') + ..aOB( + 6, + const $core.bool.fromEnvironment('protobuf.omit_field_names') + ? '' + : 'dryRun') + ..hasRequiredFields = false; + + WriteLogEntriesRequest._() : super(); + factory WriteLogEntriesRequest({ + $core.String logName, + $3.MonitoredResource resource, + $core.Map<$core.String, $core.String> labels, + $core.Iterable<$4.LogEntry> entries, + $core.bool partialSuccess, + $core.bool dryRun, + }) { + final _result = create(); + if (logName != null) { + _result.logName = logName; + } + if (resource != null) { + _result.resource = resource; + } + if (labels != null) { + _result.labels.addAll(labels); + } + if (entries != null) { + _result.entries.addAll(entries); + } + if (partialSuccess != null) { + _result.partialSuccess = partialSuccess; + } + if (dryRun != null) { + _result.dryRun = dryRun; + } + return _result; + } + factory WriteLogEntriesRequest.fromBuffer($core.List<$core.int> i, + [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromBuffer(i, r); + factory WriteLogEntriesRequest.fromJson($core.String i, + [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromJson(i, r); + @$core.Deprecated('Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.deepCopy] instead. ' + 'Will be removed in next major version') + WriteLogEntriesRequest clone() => + WriteLogEntriesRequest()..mergeFromMessage(this); + @$core.Deprecated('Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.rebuild] instead. ' + 'Will be removed in next major version') + WriteLogEntriesRequest copyWith( + void Function(WriteLogEntriesRequest) updates) => + super.copyWith((message) => updates( + message as WriteLogEntriesRequest)); // ignore: deprecated_member_use + $pb.BuilderInfo get info_ => _i; + @$core.pragma('dart2js:noInline') + static WriteLogEntriesRequest create() => WriteLogEntriesRequest._(); + WriteLogEntriesRequest createEmptyInstance() => create(); + static $pb.PbList<WriteLogEntriesRequest> createRepeated() => + $pb.PbList<WriteLogEntriesRequest>(); + @$core.pragma('dart2js:noInline') + static WriteLogEntriesRequest getDefault() => _defaultInstance ??= + $pb.GeneratedMessage.$_defaultFor<WriteLogEntriesRequest>(create); + static WriteLogEntriesRequest _defaultInstance; + + @$pb.TagNumber(1) + $core.String get logName => $_getSZ(0); + @$pb.TagNumber(1) + set logName($core.String v) { + $_setString(0, v); + } + + @$pb.TagNumber(1) + $core.bool hasLogName() => $_has(0); + @$pb.TagNumber(1) + void clearLogName() => clearField(1); + + @$pb.TagNumber(2) + $3.MonitoredResource get resource => $_getN(1); + @$pb.TagNumber(2) + set resource($3.MonitoredResource v) { + setField(2, v); + } + + @$pb.TagNumber(2) + $core.bool hasResource() => $_has(1); + @$pb.TagNumber(2) + void clearResource() => clearField(2); + @$pb.TagNumber(2) + $3.MonitoredResource ensureResource() => $_ensure(1); + + @$pb.TagNumber(3) + $core.Map<$core.String, $core.String> get labels => $_getMap(2); + + @$pb.TagNumber(4) + $core.List<$4.LogEntry> get entries => $_getList(3); + + @$pb.TagNumber(5) + $core.bool get partialSuccess => $_getBF(4); + @$pb.TagNumber(5) + set partialSuccess($core.bool v) { + $_setBool(4, v); + } + + @$pb.TagNumber(5) + $core.bool hasPartialSuccess() => $_has(4); + @$pb.TagNumber(5) + void clearPartialSuccess() => clearField(5); + + @$pb.TagNumber(6) + $core.bool get dryRun => $_getBF(5); + @$pb.TagNumber(6) + set dryRun($core.bool v) { + $_setBool(5, v); + } + + @$pb.TagNumber(6) + $core.bool hasDryRun() => $_has(5); + @$pb.TagNumber(6) + void clearDryRun() => clearField(6); +} + +class WriteLogEntriesResponse extends $pb.GeneratedMessage { + static final $pb.BuilderInfo _i = $pb.BuilderInfo( + const $core.bool.fromEnvironment('protobuf.omit_message_names') + ? '' + : 'WriteLogEntriesResponse', + package: const $pb.PackageName( + const $core.bool.fromEnvironment('protobuf.omit_message_names') + ? '' + : 'google.logging.v2'), + createEmptyInstance: create) + ..hasRequiredFields = false; + + WriteLogEntriesResponse._() : super(); + factory WriteLogEntriesResponse() => create(); + factory WriteLogEntriesResponse.fromBuffer($core.List<$core.int> i, + [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromBuffer(i, r); + factory WriteLogEntriesResponse.fromJson($core.String i, + [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromJson(i, r); + @$core.Deprecated('Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.deepCopy] instead. ' + 'Will be removed in next major version') + WriteLogEntriesResponse clone() => + WriteLogEntriesResponse()..mergeFromMessage(this); + @$core.Deprecated('Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.rebuild] instead. ' + 'Will be removed in next major version') + WriteLogEntriesResponse copyWith( + void Function(WriteLogEntriesResponse) updates) => + super.copyWith((message) => updates( + message as WriteLogEntriesResponse)); // ignore: deprecated_member_use + $pb.BuilderInfo get info_ => _i; + @$core.pragma('dart2js:noInline') + static WriteLogEntriesResponse create() => WriteLogEntriesResponse._(); + WriteLogEntriesResponse createEmptyInstance() => create(); + static $pb.PbList<WriteLogEntriesResponse> createRepeated() => + $pb.PbList<WriteLogEntriesResponse>(); + @$core.pragma('dart2js:noInline') + static WriteLogEntriesResponse getDefault() => _defaultInstance ??= + $pb.GeneratedMessage.$_defaultFor<WriteLogEntriesResponse>(create); + static WriteLogEntriesResponse _defaultInstance; +} + +class WriteLogEntriesPartialErrors extends $pb.GeneratedMessage { + static final $pb.BuilderInfo _i = $pb.BuilderInfo( + const $core.bool.fromEnvironment('protobuf.omit_message_names') + ? '' + : 'WriteLogEntriesPartialErrors', + package: const $pb.PackageName( + const $core.bool.fromEnvironment('protobuf.omit_message_names') + ? '' + : 'google.logging.v2'), + createEmptyInstance: create) + ..m<$core.int, $5.Status>( + 1, + const $core.bool.fromEnvironment('protobuf.omit_field_names') + ? '' + : 'logEntryErrors', + entryClassName: 'WriteLogEntriesPartialErrors.LogEntryErrorsEntry', + keyFieldType: $pb.PbFieldType.O3, + valueFieldType: $pb.PbFieldType.OM, + valueCreator: $5.Status.create, + packageName: const $pb.PackageName('google.logging.v2')) + ..hasRequiredFields = false; + + WriteLogEntriesPartialErrors._() : super(); + factory WriteLogEntriesPartialErrors({ + $core.Map<$core.int, $5.Status> logEntryErrors, + }) { + final _result = create(); + if (logEntryErrors != null) { + _result.logEntryErrors.addAll(logEntryErrors); + } + return _result; + } + factory WriteLogEntriesPartialErrors.fromBuffer($core.List<$core.int> i, + [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromBuffer(i, r); + factory WriteLogEntriesPartialErrors.fromJson($core.String i, + [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromJson(i, r); + @$core.Deprecated('Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.deepCopy] instead. ' + 'Will be removed in next major version') + WriteLogEntriesPartialErrors clone() => + WriteLogEntriesPartialErrors()..mergeFromMessage(this); + @$core.Deprecated('Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.rebuild] instead. ' + 'Will be removed in next major version') + WriteLogEntriesPartialErrors copyWith( + void Function(WriteLogEntriesPartialErrors) updates) => + super.copyWith((message) => updates(message + as WriteLogEntriesPartialErrors)); // ignore: deprecated_member_use + $pb.BuilderInfo get info_ => _i; + @$core.pragma('dart2js:noInline') + static WriteLogEntriesPartialErrors create() => + WriteLogEntriesPartialErrors._(); + WriteLogEntriesPartialErrors createEmptyInstance() => create(); + static $pb.PbList<WriteLogEntriesPartialErrors> createRepeated() => + $pb.PbList<WriteLogEntriesPartialErrors>(); + @$core.pragma('dart2js:noInline') + static WriteLogEntriesPartialErrors getDefault() => _defaultInstance ??= + $pb.GeneratedMessage.$_defaultFor<WriteLogEntriesPartialErrors>(create); + static WriteLogEntriesPartialErrors _defaultInstance; + + @$pb.TagNumber(1) + $core.Map<$core.int, $5.Status> get logEntryErrors => $_getMap(0); +} + +class ListLogEntriesRequest extends $pb.GeneratedMessage { + static final $pb.BuilderInfo _i = $pb.BuilderInfo( + const $core.bool.fromEnvironment('protobuf.omit_message_names') + ? '' + : 'ListLogEntriesRequest', + package: const $pb.PackageName( + const $core.bool.fromEnvironment('protobuf.omit_message_names') + ? '' + : 'google.logging.v2'), + createEmptyInstance: create) + ..aOS( + 2, + const $core.bool.fromEnvironment('protobuf.omit_field_names') + ? '' + : 'filter') + ..aOS( + 3, + const $core.bool.fromEnvironment('protobuf.omit_field_names') + ? '' + : 'orderBy') + ..a<$core.int>( + 4, + const $core.bool.fromEnvironment('protobuf.omit_field_names') + ? '' + : 'pageSize', + $pb.PbFieldType.O3) + ..aOS( + 5, + const $core.bool.fromEnvironment('protobuf.omit_field_names') + ? '' + : 'pageToken') + ..pPS( + 8, + const $core.bool.fromEnvironment('protobuf.omit_field_names') + ? '' + : 'resourceNames') + ..hasRequiredFields = false; + + ListLogEntriesRequest._() : super(); + factory ListLogEntriesRequest({ + $core.String filter, + $core.String orderBy, + $core.int pageSize, + $core.String pageToken, + $core.Iterable<$core.String> resourceNames, + }) { + final _result = create(); + if (filter != null) { + _result.filter = filter; + } + if (orderBy != null) { + _result.orderBy = orderBy; + } + if (pageSize != null) { + _result.pageSize = pageSize; + } + if (pageToken != null) { + _result.pageToken = pageToken; + } + if (resourceNames != null) { + _result.resourceNames.addAll(resourceNames); + } + return _result; + } + factory ListLogEntriesRequest.fromBuffer($core.List<$core.int> i, + [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromBuffer(i, r); + factory ListLogEntriesRequest.fromJson($core.String i, + [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromJson(i, r); + @$core.Deprecated('Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.deepCopy] instead. ' + 'Will be removed in next major version') + ListLogEntriesRequest clone() => + ListLogEntriesRequest()..mergeFromMessage(this); + @$core.Deprecated('Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.rebuild] instead. ' + 'Will be removed in next major version') + ListLogEntriesRequest copyWith( + void Function(ListLogEntriesRequest) updates) => + super.copyWith((message) => updates( + message as ListLogEntriesRequest)); // ignore: deprecated_member_use + $pb.BuilderInfo get info_ => _i; + @$core.pragma('dart2js:noInline') + static ListLogEntriesRequest create() => ListLogEntriesRequest._(); + ListLogEntriesRequest createEmptyInstance() => create(); + static $pb.PbList<ListLogEntriesRequest> createRepeated() => + $pb.PbList<ListLogEntriesRequest>(); + @$core.pragma('dart2js:noInline') + static ListLogEntriesRequest getDefault() => _defaultInstance ??= + $pb.GeneratedMessage.$_defaultFor<ListLogEntriesRequest>(create); + static ListLogEntriesRequest _defaultInstance; + + @$pb.TagNumber(2) + $core.String get filter => $_getSZ(0); + @$pb.TagNumber(2) + set filter($core.String v) { + $_setString(0, v); + } + + @$pb.TagNumber(2) + $core.bool hasFilter() => $_has(0); + @$pb.TagNumber(2) + void clearFilter() => clearField(2); + + @$pb.TagNumber(3) + $core.String get orderBy => $_getSZ(1); + @$pb.TagNumber(3) + set orderBy($core.String v) { + $_setString(1, v); + } + + @$pb.TagNumber(3) + $core.bool hasOrderBy() => $_has(1); + @$pb.TagNumber(3) + void clearOrderBy() => clearField(3); + + @$pb.TagNumber(4) + $core.int get pageSize => $_getIZ(2); + @$pb.TagNumber(4) + set pageSize($core.int v) { + $_setSignedInt32(2, v); + } + + @$pb.TagNumber(4) + $core.bool hasPageSize() => $_has(2); + @$pb.TagNumber(4) + void clearPageSize() => clearField(4); + + @$pb.TagNumber(5) + $core.String get pageToken => $_getSZ(3); + @$pb.TagNumber(5) + set pageToken($core.String v) { + $_setString(3, v); + } + + @$pb.TagNumber(5) + $core.bool hasPageToken() => $_has(3); + @$pb.TagNumber(5) + void clearPageToken() => clearField(5); + + @$pb.TagNumber(8) + $core.List<$core.String> get resourceNames => $_getList(4); +} + +class ListLogEntriesResponse extends $pb.GeneratedMessage { + static final $pb.BuilderInfo _i = $pb.BuilderInfo( + const $core.bool.fromEnvironment('protobuf.omit_message_names') + ? '' + : 'ListLogEntriesResponse', + package: const $pb.PackageName( + const $core.bool.fromEnvironment('protobuf.omit_message_names') + ? '' + : 'google.logging.v2'), + createEmptyInstance: create) + ..pc<$4.LogEntry>( + 1, + const $core.bool.fromEnvironment('protobuf.omit_field_names') + ? '' + : 'entries', + $pb.PbFieldType.PM, + subBuilder: $4.LogEntry.create) + ..aOS( + 2, + const $core.bool.fromEnvironment('protobuf.omit_field_names') + ? '' + : 'nextPageToken') + ..hasRequiredFields = false; + + ListLogEntriesResponse._() : super(); + factory ListLogEntriesResponse({ + $core.Iterable<$4.LogEntry> entries, + $core.String nextPageToken, + }) { + final _result = create(); + if (entries != null) { + _result.entries.addAll(entries); + } + if (nextPageToken != null) { + _result.nextPageToken = nextPageToken; + } + return _result; + } + factory ListLogEntriesResponse.fromBuffer($core.List<$core.int> i, + [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromBuffer(i, r); + factory ListLogEntriesResponse.fromJson($core.String i, + [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromJson(i, r); + @$core.Deprecated('Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.deepCopy] instead. ' + 'Will be removed in next major version') + ListLogEntriesResponse clone() => + ListLogEntriesResponse()..mergeFromMessage(this); + @$core.Deprecated('Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.rebuild] instead. ' + 'Will be removed in next major version') + ListLogEntriesResponse copyWith( + void Function(ListLogEntriesResponse) updates) => + super.copyWith((message) => updates( + message as ListLogEntriesResponse)); // ignore: deprecated_member_use + $pb.BuilderInfo get info_ => _i; + @$core.pragma('dart2js:noInline') + static ListLogEntriesResponse create() => ListLogEntriesResponse._(); + ListLogEntriesResponse createEmptyInstance() => create(); + static $pb.PbList<ListLogEntriesResponse> createRepeated() => + $pb.PbList<ListLogEntriesResponse>(); + @$core.pragma('dart2js:noInline') + static ListLogEntriesResponse getDefault() => _defaultInstance ??= + $pb.GeneratedMessage.$_defaultFor<ListLogEntriesResponse>(create); + static ListLogEntriesResponse _defaultInstance; + + @$pb.TagNumber(1) + $core.List<$4.LogEntry> get entries => $_getList(0); + + @$pb.TagNumber(2) + $core.String get nextPageToken => $_getSZ(1); + @$pb.TagNumber(2) + set nextPageToken($core.String v) { + $_setString(1, v); + } + + @$pb.TagNumber(2) + $core.bool hasNextPageToken() => $_has(1); + @$pb.TagNumber(2) + void clearNextPageToken() => clearField(2); +} + +class ListMonitoredResourceDescriptorsRequest extends $pb.GeneratedMessage { + static final $pb.BuilderInfo _i = $pb.BuilderInfo( + const $core.bool.fromEnvironment('protobuf.omit_message_names') + ? '' + : 'ListMonitoredResourceDescriptorsRequest', + package: const $pb.PackageName( + const $core.bool.fromEnvironment('protobuf.omit_message_names') + ? '' + : 'google.logging.v2'), + createEmptyInstance: create) + ..a<$core.int>( + 1, + const $core.bool.fromEnvironment('protobuf.omit_field_names') + ? '' + : 'pageSize', + $pb.PbFieldType.O3) + ..aOS( + 2, + const $core.bool.fromEnvironment('protobuf.omit_field_names') + ? '' + : 'pageToken') + ..hasRequiredFields = false; + + ListMonitoredResourceDescriptorsRequest._() : super(); + factory ListMonitoredResourceDescriptorsRequest({ + $core.int pageSize, + $core.String pageToken, + }) { + final _result = create(); + if (pageSize != null) { + _result.pageSize = pageSize; + } + if (pageToken != null) { + _result.pageToken = pageToken; + } + return _result; + } + factory ListMonitoredResourceDescriptorsRequest.fromBuffer( + $core.List<$core.int> i, + [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromBuffer(i, r); + factory ListMonitoredResourceDescriptorsRequest.fromJson($core.String i, + [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromJson(i, r); + @$core.Deprecated('Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.deepCopy] instead. ' + 'Will be removed in next major version') + ListMonitoredResourceDescriptorsRequest clone() => + ListMonitoredResourceDescriptorsRequest()..mergeFromMessage(this); + @$core.Deprecated('Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.rebuild] instead. ' + 'Will be removed in next major version') + ListMonitoredResourceDescriptorsRequest copyWith( + void Function(ListMonitoredResourceDescriptorsRequest) updates) => + super.copyWith((message) => updates(message + as ListMonitoredResourceDescriptorsRequest)); // ignore: deprecated_member_use + $pb.BuilderInfo get info_ => _i; + @$core.pragma('dart2js:noInline') + static ListMonitoredResourceDescriptorsRequest create() => + ListMonitoredResourceDescriptorsRequest._(); + ListMonitoredResourceDescriptorsRequest createEmptyInstance() => create(); + static $pb.PbList<ListMonitoredResourceDescriptorsRequest> createRepeated() => + $pb.PbList<ListMonitoredResourceDescriptorsRequest>(); + @$core.pragma('dart2js:noInline') + static ListMonitoredResourceDescriptorsRequest getDefault() => + _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor< + ListMonitoredResourceDescriptorsRequest>(create); + static ListMonitoredResourceDescriptorsRequest _defaultInstance; + + @$pb.TagNumber(1) + $core.int get pageSize => $_getIZ(0); + @$pb.TagNumber(1) + set pageSize($core.int v) { + $_setSignedInt32(0, v); + } + + @$pb.TagNumber(1) + $core.bool hasPageSize() => $_has(0); + @$pb.TagNumber(1) + void clearPageSize() => clearField(1); + + @$pb.TagNumber(2) + $core.String get pageToken => $_getSZ(1); + @$pb.TagNumber(2) + set pageToken($core.String v) { + $_setString(1, v); + } + + @$pb.TagNumber(2) + $core.bool hasPageToken() => $_has(1); + @$pb.TagNumber(2) + void clearPageToken() => clearField(2); +} + +class ListMonitoredResourceDescriptorsResponse extends $pb.GeneratedMessage { + static final $pb.BuilderInfo _i = $pb.BuilderInfo( + const $core.bool.fromEnvironment('protobuf.omit_message_names') + ? '' + : 'ListMonitoredResourceDescriptorsResponse', + package: const $pb.PackageName( + const $core.bool.fromEnvironment('protobuf.omit_message_names') + ? '' + : 'google.logging.v2'), + createEmptyInstance: create) + ..pc<$3.MonitoredResourceDescriptor>( + 1, + const $core.bool.fromEnvironment('protobuf.omit_field_names') + ? '' + : 'resourceDescriptors', + $pb.PbFieldType.PM, + subBuilder: $3.MonitoredResourceDescriptor.create) + ..aOS( + 2, + const $core.bool.fromEnvironment('protobuf.omit_field_names') + ? '' + : 'nextPageToken') + ..hasRequiredFields = false; + + ListMonitoredResourceDescriptorsResponse._() : super(); + factory ListMonitoredResourceDescriptorsResponse({ + $core.Iterable<$3.MonitoredResourceDescriptor> resourceDescriptors, + $core.String nextPageToken, + }) { + final _result = create(); + if (resourceDescriptors != null) { + _result.resourceDescriptors.addAll(resourceDescriptors); + } + if (nextPageToken != null) { + _result.nextPageToken = nextPageToken; + } + return _result; + } + factory ListMonitoredResourceDescriptorsResponse.fromBuffer( + $core.List<$core.int> i, + [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromBuffer(i, r); + factory ListMonitoredResourceDescriptorsResponse.fromJson($core.String i, + [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromJson(i, r); + @$core.Deprecated('Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.deepCopy] instead. ' + 'Will be removed in next major version') + ListMonitoredResourceDescriptorsResponse clone() => + ListMonitoredResourceDescriptorsResponse()..mergeFromMessage(this); + @$core.Deprecated('Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.rebuild] instead. ' + 'Will be removed in next major version') + ListMonitoredResourceDescriptorsResponse copyWith( + void Function(ListMonitoredResourceDescriptorsResponse) updates) => + super.copyWith((message) => updates(message + as ListMonitoredResourceDescriptorsResponse)); // ignore: deprecated_member_use + $pb.BuilderInfo get info_ => _i; + @$core.pragma('dart2js:noInline') + static ListMonitoredResourceDescriptorsResponse create() => + ListMonitoredResourceDescriptorsResponse._(); + ListMonitoredResourceDescriptorsResponse createEmptyInstance() => create(); + static $pb.PbList<ListMonitoredResourceDescriptorsResponse> + createRepeated() => + $pb.PbList<ListMonitoredResourceDescriptorsResponse>(); + @$core.pragma('dart2js:noInline') + static ListMonitoredResourceDescriptorsResponse getDefault() => + _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor< + ListMonitoredResourceDescriptorsResponse>(create); + static ListMonitoredResourceDescriptorsResponse _defaultInstance; + + @$pb.TagNumber(1) + $core.List<$3.MonitoredResourceDescriptor> get resourceDescriptors => + $_getList(0); + + @$pb.TagNumber(2) + $core.String get nextPageToken => $_getSZ(1); + @$pb.TagNumber(2) + set nextPageToken($core.String v) { + $_setString(1, v); + } + + @$pb.TagNumber(2) + $core.bool hasNextPageToken() => $_has(1); + @$pb.TagNumber(2) + void clearNextPageToken() => clearField(2); +} + +class ListLogsRequest extends $pb.GeneratedMessage { + static final $pb.BuilderInfo _i = $pb.BuilderInfo( + const $core.bool.fromEnvironment('protobuf.omit_message_names') + ? '' + : 'ListLogsRequest', + package: const $pb.PackageName( + const $core.bool.fromEnvironment('protobuf.omit_message_names') + ? '' + : 'google.logging.v2'), + createEmptyInstance: create) + ..aOS( + 1, + const $core.bool.fromEnvironment('protobuf.omit_field_names') + ? '' + : 'parent') + ..a<$core.int>( + 2, + const $core.bool.fromEnvironment('protobuf.omit_field_names') + ? '' + : 'pageSize', + $pb.PbFieldType.O3) + ..aOS( + 3, + const $core.bool.fromEnvironment('protobuf.omit_field_names') + ? '' + : 'pageToken') + ..pPS( + 8, + const $core.bool.fromEnvironment('protobuf.omit_field_names') + ? '' + : 'resourceNames') + ..hasRequiredFields = false; + + ListLogsRequest._() : super(); + factory ListLogsRequest({ + $core.String parent, + $core.int pageSize, + $core.String pageToken, + $core.Iterable<$core.String> resourceNames, + }) { + final _result = create(); + if (parent != null) { + _result.parent = parent; + } + if (pageSize != null) { + _result.pageSize = pageSize; + } + if (pageToken != null) { + _result.pageToken = pageToken; + } + if (resourceNames != null) { + _result.resourceNames.addAll(resourceNames); + } + return _result; + } + factory ListLogsRequest.fromBuffer($core.List<$core.int> i, + [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromBuffer(i, r); + factory ListLogsRequest.fromJson($core.String i, + [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromJson(i, r); + @$core.Deprecated('Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.deepCopy] instead. ' + 'Will be removed in next major version') + ListLogsRequest clone() => ListLogsRequest()..mergeFromMessage(this); + @$core.Deprecated('Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.rebuild] instead. ' + 'Will be removed in next major version') + ListLogsRequest copyWith(void Function(ListLogsRequest) updates) => + super.copyWith((message) => + updates(message as ListLogsRequest)); // ignore: deprecated_member_use + $pb.BuilderInfo get info_ => _i; + @$core.pragma('dart2js:noInline') + static ListLogsRequest create() => ListLogsRequest._(); + ListLogsRequest createEmptyInstance() => create(); + static $pb.PbList<ListLogsRequest> createRepeated() => + $pb.PbList<ListLogsRequest>(); + @$core.pragma('dart2js:noInline') + static ListLogsRequest getDefault() => _defaultInstance ??= + $pb.GeneratedMessage.$_defaultFor<ListLogsRequest>(create); + static ListLogsRequest _defaultInstance; + + @$pb.TagNumber(1) + $core.String get parent => $_getSZ(0); + @$pb.TagNumber(1) + set parent($core.String v) { + $_setString(0, v); + } + + @$pb.TagNumber(1) + $core.bool hasParent() => $_has(0); + @$pb.TagNumber(1) + void clearParent() => clearField(1); + + @$pb.TagNumber(2) + $core.int get pageSize => $_getIZ(1); + @$pb.TagNumber(2) + set pageSize($core.int v) { + $_setSignedInt32(1, v); + } + + @$pb.TagNumber(2) + $core.bool hasPageSize() => $_has(1); + @$pb.TagNumber(2) + void clearPageSize() => clearField(2); + + @$pb.TagNumber(3) + $core.String get pageToken => $_getSZ(2); + @$pb.TagNumber(3) + set pageToken($core.String v) { + $_setString(2, v); + } + + @$pb.TagNumber(3) + $core.bool hasPageToken() => $_has(2); + @$pb.TagNumber(3) + void clearPageToken() => clearField(3); + + @$pb.TagNumber(8) + $core.List<$core.String> get resourceNames => $_getList(3); +} + +class ListLogsResponse extends $pb.GeneratedMessage { + static final $pb.BuilderInfo _i = $pb.BuilderInfo( + const $core.bool.fromEnvironment('protobuf.omit_message_names') + ? '' + : 'ListLogsResponse', + package: const $pb.PackageName( + const $core.bool.fromEnvironment('protobuf.omit_message_names') + ? '' + : 'google.logging.v2'), + createEmptyInstance: create) + ..aOS( + 2, + const $core.bool.fromEnvironment('protobuf.omit_field_names') + ? '' + : 'nextPageToken') + ..pPS( + 3, + const $core.bool.fromEnvironment('protobuf.omit_field_names') + ? '' + : 'logNames') + ..hasRequiredFields = false; + + ListLogsResponse._() : super(); + factory ListLogsResponse({ + $core.String nextPageToken, + $core.Iterable<$core.String> logNames, + }) { + final _result = create(); + if (nextPageToken != null) { + _result.nextPageToken = nextPageToken; + } + if (logNames != null) { + _result.logNames.addAll(logNames); + } + return _result; + } + factory ListLogsResponse.fromBuffer($core.List<$core.int> i, + [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromBuffer(i, r); + factory ListLogsResponse.fromJson($core.String i, + [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromJson(i, r); + @$core.Deprecated('Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.deepCopy] instead. ' + 'Will be removed in next major version') + ListLogsResponse clone() => ListLogsResponse()..mergeFromMessage(this); + @$core.Deprecated('Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.rebuild] instead. ' + 'Will be removed in next major version') + ListLogsResponse copyWith(void Function(ListLogsResponse) updates) => + super.copyWith((message) => updates( + message as ListLogsResponse)); // ignore: deprecated_member_use + $pb.BuilderInfo get info_ => _i; + @$core.pragma('dart2js:noInline') + static ListLogsResponse create() => ListLogsResponse._(); + ListLogsResponse createEmptyInstance() => create(); + static $pb.PbList<ListLogsResponse> createRepeated() => + $pb.PbList<ListLogsResponse>(); + @$core.pragma('dart2js:noInline') + static ListLogsResponse getDefault() => _defaultInstance ??= + $pb.GeneratedMessage.$_defaultFor<ListLogsResponse>(create); + static ListLogsResponse _defaultInstance; + + @$pb.TagNumber(2) + $core.String get nextPageToken => $_getSZ(0); + @$pb.TagNumber(2) + set nextPageToken($core.String v) { + $_setString(0, v); + } + + @$pb.TagNumber(2) + $core.bool hasNextPageToken() => $_has(0); + @$pb.TagNumber(2) + void clearNextPageToken() => clearField(2); + + @$pb.TagNumber(3) + $core.List<$core.String> get logNames => $_getList(1); +} + +class TailLogEntriesRequest extends $pb.GeneratedMessage { + static final $pb.BuilderInfo _i = $pb.BuilderInfo( + const $core.bool.fromEnvironment('protobuf.omit_message_names') + ? '' + : 'TailLogEntriesRequest', + package: const $pb.PackageName( + const $core.bool.fromEnvironment('protobuf.omit_message_names') + ? '' + : 'google.logging.v2'), + createEmptyInstance: create) + ..pPS( + 1, + const $core.bool.fromEnvironment('protobuf.omit_field_names') + ? '' + : 'resourceNames') + ..aOS( + 2, + const $core.bool.fromEnvironment('protobuf.omit_field_names') + ? '' + : 'filter') + ..aOM<$6.Duration>( + 3, + const $core.bool.fromEnvironment('protobuf.omit_field_names') + ? '' + : 'bufferWindow', + subBuilder: $6.Duration.create) + ..hasRequiredFields = false; + + TailLogEntriesRequest._() : super(); + factory TailLogEntriesRequest({ + $core.Iterable<$core.String> resourceNames, + $core.String filter, + $6.Duration bufferWindow, + }) { + final _result = create(); + if (resourceNames != null) { + _result.resourceNames.addAll(resourceNames); + } + if (filter != null) { + _result.filter = filter; + } + if (bufferWindow != null) { + _result.bufferWindow = bufferWindow; + } + return _result; + } + factory TailLogEntriesRequest.fromBuffer($core.List<$core.int> i, + [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromBuffer(i, r); + factory TailLogEntriesRequest.fromJson($core.String i, + [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromJson(i, r); + @$core.Deprecated('Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.deepCopy] instead. ' + 'Will be removed in next major version') + TailLogEntriesRequest clone() => + TailLogEntriesRequest()..mergeFromMessage(this); + @$core.Deprecated('Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.rebuild] instead. ' + 'Will be removed in next major version') + TailLogEntriesRequest copyWith( + void Function(TailLogEntriesRequest) updates) => + super.copyWith((message) => updates( + message as TailLogEntriesRequest)); // ignore: deprecated_member_use + $pb.BuilderInfo get info_ => _i; + @$core.pragma('dart2js:noInline') + static TailLogEntriesRequest create() => TailLogEntriesRequest._(); + TailLogEntriesRequest createEmptyInstance() => create(); + static $pb.PbList<TailLogEntriesRequest> createRepeated() => + $pb.PbList<TailLogEntriesRequest>(); + @$core.pragma('dart2js:noInline') + static TailLogEntriesRequest getDefault() => _defaultInstance ??= + $pb.GeneratedMessage.$_defaultFor<TailLogEntriesRequest>(create); + static TailLogEntriesRequest _defaultInstance; + + @$pb.TagNumber(1) + $core.List<$core.String> get resourceNames => $_getList(0); + + @$pb.TagNumber(2) + $core.String get filter => $_getSZ(1); + @$pb.TagNumber(2) + set filter($core.String v) { + $_setString(1, v); + } + + @$pb.TagNumber(2) + $core.bool hasFilter() => $_has(1); + @$pb.TagNumber(2) + void clearFilter() => clearField(2); + + @$pb.TagNumber(3) + $6.Duration get bufferWindow => $_getN(2); + @$pb.TagNumber(3) + set bufferWindow($6.Duration v) { + setField(3, v); + } + + @$pb.TagNumber(3) + $core.bool hasBufferWindow() => $_has(2); + @$pb.TagNumber(3) + void clearBufferWindow() => clearField(3); + @$pb.TagNumber(3) + $6.Duration ensureBufferWindow() => $_ensure(2); +} + +class TailLogEntriesResponse_SuppressionInfo extends $pb.GeneratedMessage { + static final $pb.BuilderInfo _i = $pb.BuilderInfo( + const $core.bool.fromEnvironment('protobuf.omit_message_names') + ? '' + : 'TailLogEntriesResponse.SuppressionInfo', + package: const $pb.PackageName( + const $core.bool.fromEnvironment('protobuf.omit_message_names') + ? '' + : 'google.logging.v2'), + createEmptyInstance: create) + ..e<TailLogEntriesResponse_SuppressionInfo_Reason>( + 1, + const $core.bool.fromEnvironment('protobuf.omit_field_names') + ? '' + : 'reason', + $pb.PbFieldType.OE, + defaultOrMaker: + TailLogEntriesResponse_SuppressionInfo_Reason.REASON_UNSPECIFIED, + valueOf: TailLogEntriesResponse_SuppressionInfo_Reason.valueOf, + enumValues: TailLogEntriesResponse_SuppressionInfo_Reason.values) + ..a<$core.int>( + 2, + const $core.bool.fromEnvironment('protobuf.omit_field_names') + ? '' + : 'suppressedCount', + $pb.PbFieldType.O3) + ..hasRequiredFields = false; + + TailLogEntriesResponse_SuppressionInfo._() : super(); + factory TailLogEntriesResponse_SuppressionInfo({ + TailLogEntriesResponse_SuppressionInfo_Reason reason, + $core.int suppressedCount, + }) { + final _result = create(); + if (reason != null) { + _result.reason = reason; + } + if (suppressedCount != null) { + _result.suppressedCount = suppressedCount; + } + return _result; + } + factory TailLogEntriesResponse_SuppressionInfo.fromBuffer( + $core.List<$core.int> i, + [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromBuffer(i, r); + factory TailLogEntriesResponse_SuppressionInfo.fromJson($core.String i, + [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromJson(i, r); + @$core.Deprecated('Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.deepCopy] instead. ' + 'Will be removed in next major version') + TailLogEntriesResponse_SuppressionInfo clone() => + TailLogEntriesResponse_SuppressionInfo()..mergeFromMessage(this); + @$core.Deprecated('Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.rebuild] instead. ' + 'Will be removed in next major version') + TailLogEntriesResponse_SuppressionInfo copyWith( + void Function(TailLogEntriesResponse_SuppressionInfo) updates) => + super.copyWith((message) => updates(message + as TailLogEntriesResponse_SuppressionInfo)); // ignore: deprecated_member_use + $pb.BuilderInfo get info_ => _i; + @$core.pragma('dart2js:noInline') + static TailLogEntriesResponse_SuppressionInfo create() => + TailLogEntriesResponse_SuppressionInfo._(); + TailLogEntriesResponse_SuppressionInfo createEmptyInstance() => create(); + static $pb.PbList<TailLogEntriesResponse_SuppressionInfo> createRepeated() => + $pb.PbList<TailLogEntriesResponse_SuppressionInfo>(); + @$core.pragma('dart2js:noInline') + static TailLogEntriesResponse_SuppressionInfo getDefault() => + _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor< + TailLogEntriesResponse_SuppressionInfo>(create); + static TailLogEntriesResponse_SuppressionInfo _defaultInstance; + + @$pb.TagNumber(1) + TailLogEntriesResponse_SuppressionInfo_Reason get reason => $_getN(0); + @$pb.TagNumber(1) + set reason(TailLogEntriesResponse_SuppressionInfo_Reason v) { + setField(1, v); + } + + @$pb.TagNumber(1) + $core.bool hasReason() => $_has(0); + @$pb.TagNumber(1) + void clearReason() => clearField(1); + + @$pb.TagNumber(2) + $core.int get suppressedCount => $_getIZ(1); + @$pb.TagNumber(2) + set suppressedCount($core.int v) { + $_setSignedInt32(1, v); + } + + @$pb.TagNumber(2) + $core.bool hasSuppressedCount() => $_has(1); + @$pb.TagNumber(2) + void clearSuppressedCount() => clearField(2); +} + +class TailLogEntriesResponse extends $pb.GeneratedMessage { + static final $pb.BuilderInfo _i = $pb.BuilderInfo( + const $core.bool.fromEnvironment('protobuf.omit_message_names') + ? '' + : 'TailLogEntriesResponse', + package: const $pb.PackageName( + const $core.bool.fromEnvironment('protobuf.omit_message_names') + ? '' + : 'google.logging.v2'), + createEmptyInstance: create) + ..pc<$4.LogEntry>( + 1, + const $core.bool.fromEnvironment('protobuf.omit_field_names') + ? '' + : 'entries', + $pb.PbFieldType.PM, + subBuilder: $4.LogEntry.create) + ..pc<TailLogEntriesResponse_SuppressionInfo>( + 2, + const $core.bool.fromEnvironment('protobuf.omit_field_names') + ? '' + : 'suppressionInfo', + $pb.PbFieldType.PM, + subBuilder: TailLogEntriesResponse_SuppressionInfo.create) + ..hasRequiredFields = false; + + TailLogEntriesResponse._() : super(); + factory TailLogEntriesResponse({ + $core.Iterable<$4.LogEntry> entries, + $core.Iterable<TailLogEntriesResponse_SuppressionInfo> suppressionInfo, + }) { + final _result = create(); + if (entries != null) { + _result.entries.addAll(entries); + } + if (suppressionInfo != null) { + _result.suppressionInfo.addAll(suppressionInfo); + } + return _result; + } + factory TailLogEntriesResponse.fromBuffer($core.List<$core.int> i, + [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromBuffer(i, r); + factory TailLogEntriesResponse.fromJson($core.String i, + [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromJson(i, r); + @$core.Deprecated('Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.deepCopy] instead. ' + 'Will be removed in next major version') + TailLogEntriesResponse clone() => + TailLogEntriesResponse()..mergeFromMessage(this); + @$core.Deprecated('Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.rebuild] instead. ' + 'Will be removed in next major version') + TailLogEntriesResponse copyWith( + void Function(TailLogEntriesResponse) updates) => + super.copyWith((message) => updates( + message as TailLogEntriesResponse)); // ignore: deprecated_member_use + $pb.BuilderInfo get info_ => _i; + @$core.pragma('dart2js:noInline') + static TailLogEntriesResponse create() => TailLogEntriesResponse._(); + TailLogEntriesResponse createEmptyInstance() => create(); + static $pb.PbList<TailLogEntriesResponse> createRepeated() => + $pb.PbList<TailLogEntriesResponse>(); + @$core.pragma('dart2js:noInline') + static TailLogEntriesResponse getDefault() => _defaultInstance ??= + $pb.GeneratedMessage.$_defaultFor<TailLogEntriesResponse>(create); + static TailLogEntriesResponse _defaultInstance; + + @$pb.TagNumber(1) + $core.List<$4.LogEntry> get entries => $_getList(0); + + @$pb.TagNumber(2) + $core.List<TailLogEntriesResponse_SuppressionInfo> get suppressionInfo => + $_getList(1); +}
diff --git a/grpc/example/googleapis/lib/src/generated/google/logging/v2/logging.pbenum.dart b/grpc/example/googleapis/lib/src/generated/google/logging/v2/logging.pbenum.dart new file mode 100644 index 0000000..1172f67 --- /dev/null +++ b/grpc/example/googleapis/lib/src/generated/google/logging/v2/logging.pbenum.dart
@@ -0,0 +1,49 @@ +/// +// Generated code. Do not modify. +// source: google/logging/v2/logging.proto +// +// @dart = 2.7 +// ignore_for_file: annotate_overrides,camel_case_types,unnecessary_const,non_constant_identifier_names,library_prefixes,unused_import,unused_shown_name,return_of_invalid_type,unnecessary_this,prefer_final_fields + +// ignore_for_file: UNDEFINED_SHOWN_NAME +import 'dart:core' as $core; +import 'package:protobuf/protobuf.dart' as $pb; + +class TailLogEntriesResponse_SuppressionInfo_Reason extends $pb.ProtobufEnum { + static const TailLogEntriesResponse_SuppressionInfo_Reason + REASON_UNSPECIFIED = TailLogEntriesResponse_SuppressionInfo_Reason._( + 0, + const $core.bool.fromEnvironment('protobuf.omit_enum_names') + ? '' + : 'REASON_UNSPECIFIED'); + static const TailLogEntriesResponse_SuppressionInfo_Reason RATE_LIMIT = + TailLogEntriesResponse_SuppressionInfo_Reason._( + 1, + const $core.bool.fromEnvironment('protobuf.omit_enum_names') + ? '' + : 'RATE_LIMIT'); + static const TailLogEntriesResponse_SuppressionInfo_Reason NOT_CONSUMED = + TailLogEntriesResponse_SuppressionInfo_Reason._( + 2, + const $core.bool.fromEnvironment('protobuf.omit_enum_names') + ? '' + : 'NOT_CONSUMED'); + + static const $core.List<TailLogEntriesResponse_SuppressionInfo_Reason> + values = <TailLogEntriesResponse_SuppressionInfo_Reason>[ + REASON_UNSPECIFIED, + RATE_LIMIT, + NOT_CONSUMED, + ]; + + static final $core + .Map<$core.int, TailLogEntriesResponse_SuppressionInfo_Reason> + _byValue = $pb.ProtobufEnum.initByValue(values); + static TailLogEntriesResponse_SuppressionInfo_Reason valueOf( + $core.int value) => + _byValue[value]; + + const TailLogEntriesResponse_SuppressionInfo_Reason._( + $core.int v, $core.String n) + : super(v, n); +}
diff --git a/grpc/example/googleapis/lib/src/generated/google/logging/v2/logging.pbgrpc.dart b/grpc/example/googleapis/lib/src/generated/google/logging/v2/logging.pbgrpc.dart new file mode 100644 index 0000000..0aad83b --- /dev/null +++ b/grpc/example/googleapis/lib/src/generated/google/logging/v2/logging.pbgrpc.dart
@@ -0,0 +1,197 @@ +/// +// Generated code. Do not modify. +// source: google/logging/v2/logging.proto +// +// @dart = 2.7 +// ignore_for_file: annotate_overrides,camel_case_types,unnecessary_const,non_constant_identifier_names,library_prefixes,unused_import,unused_shown_name,return_of_invalid_type,unnecessary_this,prefer_final_fields + +import 'dart:async' as $async; + +import 'dart:core' as $core; + +import 'package:grpc/service_api.dart' as $grpc; +import 'logging.pb.dart' as $2; +import '../../protobuf/empty.pb.dart' as $1; +export 'logging.pb.dart'; + +class LoggingServiceV2Client extends $grpc.Client { + static final _$deleteLog = $grpc.ClientMethod<$2.DeleteLogRequest, $1.Empty>( + '/google.logging.v2.LoggingServiceV2/DeleteLog', + ($2.DeleteLogRequest value) => value.writeToBuffer(), + ($core.List<$core.int> value) => $1.Empty.fromBuffer(value)); + static final _$writeLogEntries = + $grpc.ClientMethod<$2.WriteLogEntriesRequest, $2.WriteLogEntriesResponse>( + '/google.logging.v2.LoggingServiceV2/WriteLogEntries', + ($2.WriteLogEntriesRequest value) => value.writeToBuffer(), + ($core.List<$core.int> value) => + $2.WriteLogEntriesResponse.fromBuffer(value)); + static final _$listLogEntries = + $grpc.ClientMethod<$2.ListLogEntriesRequest, $2.ListLogEntriesResponse>( + '/google.logging.v2.LoggingServiceV2/ListLogEntries', + ($2.ListLogEntriesRequest value) => value.writeToBuffer(), + ($core.List<$core.int> value) => + $2.ListLogEntriesResponse.fromBuffer(value)); + static final _$listMonitoredResourceDescriptors = $grpc.ClientMethod< + $2.ListMonitoredResourceDescriptorsRequest, + $2.ListMonitoredResourceDescriptorsResponse>( + '/google.logging.v2.LoggingServiceV2/ListMonitoredResourceDescriptors', + ($2.ListMonitoredResourceDescriptorsRequest value) => + value.writeToBuffer(), + ($core.List<$core.int> value) => + $2.ListMonitoredResourceDescriptorsResponse.fromBuffer(value)); + static final _$listLogs = + $grpc.ClientMethod<$2.ListLogsRequest, $2.ListLogsResponse>( + '/google.logging.v2.LoggingServiceV2/ListLogs', + ($2.ListLogsRequest value) => value.writeToBuffer(), + ($core.List<$core.int> value) => + $2.ListLogsResponse.fromBuffer(value)); + static final _$tailLogEntries = + $grpc.ClientMethod<$2.TailLogEntriesRequest, $2.TailLogEntriesResponse>( + '/google.logging.v2.LoggingServiceV2/TailLogEntries', + ($2.TailLogEntriesRequest value) => value.writeToBuffer(), + ($core.List<$core.int> value) => + $2.TailLogEntriesResponse.fromBuffer(value)); + + LoggingServiceV2Client($grpc.ClientChannel channel, + {$grpc.CallOptions options, + $core.Iterable<$grpc.ClientInterceptor> interceptors}) + : super(channel, options: options, interceptors: interceptors); + + $grpc.ResponseFuture<$1.Empty> deleteLog($2.DeleteLogRequest request, + {$grpc.CallOptions options}) { + return $createUnaryCall(_$deleteLog, request, options: options); + } + + $grpc.ResponseFuture<$2.WriteLogEntriesResponse> writeLogEntries( + $2.WriteLogEntriesRequest request, + {$grpc.CallOptions options}) { + return $createUnaryCall(_$writeLogEntries, request, options: options); + } + + $grpc.ResponseFuture<$2.ListLogEntriesResponse> listLogEntries( + $2.ListLogEntriesRequest request, + {$grpc.CallOptions options}) { + return $createUnaryCall(_$listLogEntries, request, options: options); + } + + $grpc.ResponseFuture<$2.ListMonitoredResourceDescriptorsResponse> + listMonitoredResourceDescriptors( + $2.ListMonitoredResourceDescriptorsRequest request, + {$grpc.CallOptions options}) { + return $createUnaryCall(_$listMonitoredResourceDescriptors, request, + options: options); + } + + $grpc.ResponseFuture<$2.ListLogsResponse> listLogs($2.ListLogsRequest request, + {$grpc.CallOptions options}) { + return $createUnaryCall(_$listLogs, request, options: options); + } + + $grpc.ResponseStream<$2.TailLogEntriesResponse> tailLogEntries( + $async.Stream<$2.TailLogEntriesRequest> request, + {$grpc.CallOptions options}) { + return $createStreamingCall(_$tailLogEntries, request, options: options); + } +} + +abstract class LoggingServiceV2ServiceBase extends $grpc.Service { + $core.String get $name => 'google.logging.v2.LoggingServiceV2'; + + LoggingServiceV2ServiceBase() { + $addMethod($grpc.ServiceMethod<$2.DeleteLogRequest, $1.Empty>( + 'DeleteLog', + deleteLog_Pre, + false, + false, + ($core.List<$core.int> value) => $2.DeleteLogRequest.fromBuffer(value), + ($1.Empty value) => value.writeToBuffer())); + $addMethod($grpc.ServiceMethod<$2.WriteLogEntriesRequest, + $2.WriteLogEntriesResponse>( + 'WriteLogEntries', + writeLogEntries_Pre, + false, + false, + ($core.List<$core.int> value) => + $2.WriteLogEntriesRequest.fromBuffer(value), + ($2.WriteLogEntriesResponse value) => value.writeToBuffer())); + $addMethod($grpc.ServiceMethod<$2.ListLogEntriesRequest, + $2.ListLogEntriesResponse>( + 'ListLogEntries', + listLogEntries_Pre, + false, + false, + ($core.List<$core.int> value) => + $2.ListLogEntriesRequest.fromBuffer(value), + ($2.ListLogEntriesResponse value) => value.writeToBuffer())); + $addMethod($grpc.ServiceMethod<$2.ListMonitoredResourceDescriptorsRequest, + $2.ListMonitoredResourceDescriptorsResponse>( + 'ListMonitoredResourceDescriptors', + listMonitoredResourceDescriptors_Pre, + false, + false, + ($core.List<$core.int> value) => + $2.ListMonitoredResourceDescriptorsRequest.fromBuffer(value), + ($2.ListMonitoredResourceDescriptorsResponse value) => + value.writeToBuffer())); + $addMethod($grpc.ServiceMethod<$2.ListLogsRequest, $2.ListLogsResponse>( + 'ListLogs', + listLogs_Pre, + false, + false, + ($core.List<$core.int> value) => $2.ListLogsRequest.fromBuffer(value), + ($2.ListLogsResponse value) => value.writeToBuffer())); + $addMethod($grpc.ServiceMethod<$2.TailLogEntriesRequest, + $2.TailLogEntriesResponse>( + 'TailLogEntries', + tailLogEntries, + true, + true, + ($core.List<$core.int> value) => + $2.TailLogEntriesRequest.fromBuffer(value), + ($2.TailLogEntriesResponse value) => value.writeToBuffer())); + } + + $async.Future<$1.Empty> deleteLog_Pre($grpc.ServiceCall call, + $async.Future<$2.DeleteLogRequest> request) async { + return deleteLog(call, await request); + } + + $async.Future<$2.WriteLogEntriesResponse> writeLogEntries_Pre( + $grpc.ServiceCall call, + $async.Future<$2.WriteLogEntriesRequest> request) async { + return writeLogEntries(call, await request); + } + + $async.Future<$2.ListLogEntriesResponse> listLogEntries_Pre( + $grpc.ServiceCall call, + $async.Future<$2.ListLogEntriesRequest> request) async { + return listLogEntries(call, await request); + } + + $async.Future<$2.ListMonitoredResourceDescriptorsResponse> + listMonitoredResourceDescriptors_Pre( + $grpc.ServiceCall call, + $async.Future<$2.ListMonitoredResourceDescriptorsRequest> + request) async { + return listMonitoredResourceDescriptors(call, await request); + } + + $async.Future<$2.ListLogsResponse> listLogs_Pre( + $grpc.ServiceCall call, $async.Future<$2.ListLogsRequest> request) async { + return listLogs(call, await request); + } + + $async.Future<$1.Empty> deleteLog( + $grpc.ServiceCall call, $2.DeleteLogRequest request); + $async.Future<$2.WriteLogEntriesResponse> writeLogEntries( + $grpc.ServiceCall call, $2.WriteLogEntriesRequest request); + $async.Future<$2.ListLogEntriesResponse> listLogEntries( + $grpc.ServiceCall call, $2.ListLogEntriesRequest request); + $async.Future<$2.ListMonitoredResourceDescriptorsResponse> + listMonitoredResourceDescriptors($grpc.ServiceCall call, + $2.ListMonitoredResourceDescriptorsRequest request); + $async.Future<$2.ListLogsResponse> listLogs( + $grpc.ServiceCall call, $2.ListLogsRequest request); + $async.Stream<$2.TailLogEntriesResponse> tailLogEntries( + $grpc.ServiceCall call, $async.Stream<$2.TailLogEntriesRequest> request); +}
diff --git a/grpc/example/googleapis/lib/src/generated/google/logging/v2/logging.pbjson.dart b/grpc/example/googleapis/lib/src/generated/google/logging/v2/logging.pbjson.dart new file mode 100644 index 0000000..aa64628 --- /dev/null +++ b/grpc/example/googleapis/lib/src/generated/google/logging/v2/logging.pbjson.dart
@@ -0,0 +1,370 @@ +/// +// Generated code. Do not modify. +// source: google/logging/v2/logging.proto +// +// @dart = 2.7 +// ignore_for_file: annotate_overrides,camel_case_types,unnecessary_const,non_constant_identifier_names,library_prefixes,unused_import,unused_shown_name,return_of_invalid_type,unnecessary_this,prefer_final_fields + +const DeleteLogRequest$json = const { + '1': 'DeleteLogRequest', + '2': const [ + const { + '1': 'log_name', + '3': 1, + '4': 1, + '5': 9, + '8': const {}, + '10': 'logName' + }, + ], +}; + +const WriteLogEntriesRequest$json = const { + '1': 'WriteLogEntriesRequest', + '2': const [ + const { + '1': 'log_name', + '3': 1, + '4': 1, + '5': 9, + '8': const {}, + '10': 'logName' + }, + const { + '1': 'resource', + '3': 2, + '4': 1, + '5': 11, + '6': '.google.api.MonitoredResource', + '8': const {}, + '10': 'resource' + }, + const { + '1': 'labels', + '3': 3, + '4': 3, + '5': 11, + '6': '.google.logging.v2.WriteLogEntriesRequest.LabelsEntry', + '8': const {}, + '10': 'labels' + }, + const { + '1': 'entries', + '3': 4, + '4': 3, + '5': 11, + '6': '.google.logging.v2.LogEntry', + '8': const {}, + '10': 'entries' + }, + const { + '1': 'partial_success', + '3': 5, + '4': 1, + '5': 8, + '8': const {}, + '10': 'partialSuccess' + }, + const { + '1': 'dry_run', + '3': 6, + '4': 1, + '5': 8, + '8': const {}, + '10': 'dryRun' + }, + ], + '3': const [WriteLogEntriesRequest_LabelsEntry$json], +}; + +const WriteLogEntriesRequest_LabelsEntry$json = const { + '1': 'LabelsEntry', + '2': const [ + const {'1': 'key', '3': 1, '4': 1, '5': 9, '10': 'key'}, + const {'1': 'value', '3': 2, '4': 1, '5': 9, '10': 'value'}, + ], + '7': const {'7': true}, +}; + +const WriteLogEntriesResponse$json = const { + '1': 'WriteLogEntriesResponse', +}; + +const WriteLogEntriesPartialErrors$json = const { + '1': 'WriteLogEntriesPartialErrors', + '2': const [ + const { + '1': 'log_entry_errors', + '3': 1, + '4': 3, + '5': 11, + '6': + '.google.logging.v2.WriteLogEntriesPartialErrors.LogEntryErrorsEntry', + '10': 'logEntryErrors' + }, + ], + '3': const [WriteLogEntriesPartialErrors_LogEntryErrorsEntry$json], +}; + +const WriteLogEntriesPartialErrors_LogEntryErrorsEntry$json = const { + '1': 'LogEntryErrorsEntry', + '2': const [ + const {'1': 'key', '3': 1, '4': 1, '5': 5, '10': 'key'}, + const { + '1': 'value', + '3': 2, + '4': 1, + '5': 11, + '6': '.google.rpc.Status', + '10': 'value' + }, + ], + '7': const {'7': true}, +}; + +const ListLogEntriesRequest$json = const { + '1': 'ListLogEntriesRequest', + '2': const [ + const { + '1': 'resource_names', + '3': 8, + '4': 3, + '5': 9, + '8': const {}, + '10': 'resourceNames' + }, + const { + '1': 'filter', + '3': 2, + '4': 1, + '5': 9, + '8': const {}, + '10': 'filter' + }, + const { + '1': 'order_by', + '3': 3, + '4': 1, + '5': 9, + '8': const {}, + '10': 'orderBy' + }, + const { + '1': 'page_size', + '3': 4, + '4': 1, + '5': 5, + '8': const {}, + '10': 'pageSize' + }, + const { + '1': 'page_token', + '3': 5, + '4': 1, + '5': 9, + '8': const {}, + '10': 'pageToken' + }, + ], +}; + +const ListLogEntriesResponse$json = const { + '1': 'ListLogEntriesResponse', + '2': const [ + const { + '1': 'entries', + '3': 1, + '4': 3, + '5': 11, + '6': '.google.logging.v2.LogEntry', + '10': 'entries' + }, + const { + '1': 'next_page_token', + '3': 2, + '4': 1, + '5': 9, + '10': 'nextPageToken' + }, + ], +}; + +const ListMonitoredResourceDescriptorsRequest$json = const { + '1': 'ListMonitoredResourceDescriptorsRequest', + '2': const [ + const { + '1': 'page_size', + '3': 1, + '4': 1, + '5': 5, + '8': const {}, + '10': 'pageSize' + }, + const { + '1': 'page_token', + '3': 2, + '4': 1, + '5': 9, + '8': const {}, + '10': 'pageToken' + }, + ], +}; + +const ListMonitoredResourceDescriptorsResponse$json = const { + '1': 'ListMonitoredResourceDescriptorsResponse', + '2': const [ + const { + '1': 'resource_descriptors', + '3': 1, + '4': 3, + '5': 11, + '6': '.google.api.MonitoredResourceDescriptor', + '10': 'resourceDescriptors' + }, + const { + '1': 'next_page_token', + '3': 2, + '4': 1, + '5': 9, + '10': 'nextPageToken' + }, + ], +}; + +const ListLogsRequest$json = const { + '1': 'ListLogsRequest', + '2': const [ + const { + '1': 'parent', + '3': 1, + '4': 1, + '5': 9, + '8': const {}, + '10': 'parent' + }, + const { + '1': 'page_size', + '3': 2, + '4': 1, + '5': 5, + '8': const {}, + '10': 'pageSize' + }, + const { + '1': 'page_token', + '3': 3, + '4': 1, + '5': 9, + '8': const {}, + '10': 'pageToken' + }, + const { + '1': 'resource_names', + '3': 8, + '4': 3, + '5': 9, + '8': const {}, + '10': 'resourceNames' + }, + ], +}; + +const ListLogsResponse$json = const { + '1': 'ListLogsResponse', + '2': const [ + const {'1': 'log_names', '3': 3, '4': 3, '5': 9, '10': 'logNames'}, + const { + '1': 'next_page_token', + '3': 2, + '4': 1, + '5': 9, + '10': 'nextPageToken' + }, + ], +}; + +const TailLogEntriesRequest$json = const { + '1': 'TailLogEntriesRequest', + '2': const [ + const { + '1': 'resource_names', + '3': 1, + '4': 3, + '5': 9, + '8': const {}, + '10': 'resourceNames' + }, + const { + '1': 'filter', + '3': 2, + '4': 1, + '5': 9, + '8': const {}, + '10': 'filter' + }, + const { + '1': 'buffer_window', + '3': 3, + '4': 1, + '5': 11, + '6': '.google.protobuf.Duration', + '8': const {}, + '10': 'bufferWindow' + }, + ], +}; + +const TailLogEntriesResponse$json = const { + '1': 'TailLogEntriesResponse', + '2': const [ + const { + '1': 'entries', + '3': 1, + '4': 3, + '5': 11, + '6': '.google.logging.v2.LogEntry', + '10': 'entries' + }, + const { + '1': 'suppression_info', + '3': 2, + '4': 3, + '5': 11, + '6': '.google.logging.v2.TailLogEntriesResponse.SuppressionInfo', + '10': 'suppressionInfo' + }, + ], + '3': const [TailLogEntriesResponse_SuppressionInfo$json], +}; + +const TailLogEntriesResponse_SuppressionInfo$json = const { + '1': 'SuppressionInfo', + '2': const [ + const { + '1': 'reason', + '3': 1, + '4': 1, + '5': 14, + '6': '.google.logging.v2.TailLogEntriesResponse.SuppressionInfo.Reason', + '10': 'reason' + }, + const { + '1': 'suppressed_count', + '3': 2, + '4': 1, + '5': 5, + '10': 'suppressedCount' + }, + ], + '4': const [TailLogEntriesResponse_SuppressionInfo_Reason$json], +}; + +const TailLogEntriesResponse_SuppressionInfo_Reason$json = const { + '1': 'Reason', + '2': const [ + const {'1': 'REASON_UNSPECIFIED', '2': 0}, + const {'1': 'RATE_LIMIT', '2': 1}, + const {'1': 'NOT_CONSUMED', '2': 2}, + ], +};
diff --git a/grpc/example/googleapis/lib/src/generated/google/protobuf/any.pb.dart b/grpc/example/googleapis/lib/src/generated/google/protobuf/any.pb.dart new file mode 100644 index 0000000..42c5cc2 --- /dev/null +++ b/grpc/example/googleapis/lib/src/generated/google/protobuf/any.pb.dart
@@ -0,0 +1,112 @@ +/// +// Generated code. Do not modify. +// source: google/protobuf/any.proto +// +// @dart = 2.7 +// ignore_for_file: annotate_overrides,camel_case_types,unnecessary_const,non_constant_identifier_names,library_prefixes,unused_import,unused_shown_name,return_of_invalid_type,unnecessary_this,prefer_final_fields + +import 'dart:core' as $core; + +import 'package:protobuf/protobuf.dart' as $pb; + +import 'package:protobuf/src/protobuf/mixins/well_known.dart' as $mixin; + +class Any extends $pb.GeneratedMessage with $mixin.AnyMixin { + static final $pb.BuilderInfo _i = $pb.BuilderInfo( + const $core.bool.fromEnvironment('protobuf.omit_message_names') + ? '' + : 'Any', + package: const $pb.PackageName( + const $core.bool.fromEnvironment('protobuf.omit_message_names') + ? '' + : 'google.protobuf'), + createEmptyInstance: create, + toProto3Json: $mixin.AnyMixin.toProto3JsonHelper, + fromProto3Json: $mixin.AnyMixin.fromProto3JsonHelper) + ..aOS( + 1, + const $core.bool.fromEnvironment('protobuf.omit_field_names') + ? '' + : 'typeUrl') + ..a<$core.List<$core.int>>( + 2, + const $core.bool.fromEnvironment('protobuf.omit_field_names') + ? '' + : 'value', + $pb.PbFieldType.OY) + ..hasRequiredFields = false; + + Any._() : super(); + factory Any({ + $core.String typeUrl, + $core.List<$core.int> value, + }) { + final _result = create(); + if (typeUrl != null) { + _result.typeUrl = typeUrl; + } + if (value != null) { + _result.value = value; + } + return _result; + } + factory Any.fromBuffer($core.List<$core.int> i, + [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromBuffer(i, r); + factory Any.fromJson($core.String i, + [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromJson(i, r); + @$core.Deprecated('Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.deepCopy] instead. ' + 'Will be removed in next major version') + Any clone() => Any()..mergeFromMessage(this); + @$core.Deprecated('Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.rebuild] instead. ' + 'Will be removed in next major version') + Any copyWith(void Function(Any) updates) => super.copyWith( + (message) => updates(message as Any)); // ignore: deprecated_member_use + $pb.BuilderInfo get info_ => _i; + @$core.pragma('dart2js:noInline') + static Any create() => Any._(); + Any createEmptyInstance() => create(); + static $pb.PbList<Any> createRepeated() => $pb.PbList<Any>(); + @$core.pragma('dart2js:noInline') + static Any getDefault() => + _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor<Any>(create); + static Any _defaultInstance; + + @$pb.TagNumber(1) + $core.String get typeUrl => $_getSZ(0); + @$pb.TagNumber(1) + set typeUrl($core.String v) { + $_setString(0, v); + } + + @$pb.TagNumber(1) + $core.bool hasTypeUrl() => $_has(0); + @$pb.TagNumber(1) + void clearTypeUrl() => clearField(1); + + @$pb.TagNumber(2) + $core.List<$core.int> get value => $_getN(1); + @$pb.TagNumber(2) + set value($core.List<$core.int> v) { + $_setBytes(1, v); + } + + @$pb.TagNumber(2) + $core.bool hasValue() => $_has(1); + @$pb.TagNumber(2) + void clearValue() => clearField(2); + + /// Creates a new [Any] encoding [message]. + /// + /// The [typeUrl] will be [typeUrlPrefix]/`fullName` where `fullName` is + /// the fully qualified name of the type of [message]. + static Any pack($pb.GeneratedMessage message, + {$core.String typeUrlPrefix = 'type.googleapis.com'}) { + final result = create(); + $mixin.AnyMixin.packIntoAny(result, message, typeUrlPrefix: typeUrlPrefix); + return result; + } +}
diff --git a/grpc/example/googleapis/lib/src/generated/google/protobuf/any.pbenum.dart b/grpc/example/googleapis/lib/src/generated/google/protobuf/any.pbenum.dart new file mode 100644 index 0000000..bdb145d --- /dev/null +++ b/grpc/example/googleapis/lib/src/generated/google/protobuf/any.pbenum.dart
@@ -0,0 +1,6 @@ +/// +// Generated code. Do not modify. +// source: google/protobuf/any.proto +// +// @dart = 2.7 +// ignore_for_file: annotate_overrides,camel_case_types,unnecessary_const,non_constant_identifier_names,library_prefixes,unused_import,unused_shown_name,return_of_invalid_type,unnecessary_this,prefer_final_fields
diff --git a/grpc/example/googleapis/lib/src/generated/google/protobuf/any.pbjson.dart b/grpc/example/googleapis/lib/src/generated/google/protobuf/any.pbjson.dart new file mode 100644 index 0000000..2ac5a10 --- /dev/null +++ b/grpc/example/googleapis/lib/src/generated/google/protobuf/any.pbjson.dart
@@ -0,0 +1,14 @@ +/// +// Generated code. Do not modify. +// source: google/protobuf/any.proto +// +// @dart = 2.7 +// ignore_for_file: annotate_overrides,camel_case_types,unnecessary_const,non_constant_identifier_names,library_prefixes,unused_import,unused_shown_name,return_of_invalid_type,unnecessary_this,prefer_final_fields + +const Any$json = const { + '1': 'Any', + '2': const [ + const {'1': 'type_url', '3': 1, '4': 1, '5': 9, '10': 'typeUrl'}, + const {'1': 'value', '3': 2, '4': 1, '5': 12, '10': 'value'}, + ], +};
diff --git a/grpc/example/googleapis/lib/src/generated/google/protobuf/duration.pb.dart b/grpc/example/googleapis/lib/src/generated/google/protobuf/duration.pb.dart new file mode 100644 index 0000000..ab3e5c3 --- /dev/null +++ b/grpc/example/googleapis/lib/src/generated/google/protobuf/duration.pb.dart
@@ -0,0 +1,103 @@ +/// +// Generated code. Do not modify. +// source: google/protobuf/duration.proto +// +// @dart = 2.7 +// ignore_for_file: annotate_overrides,camel_case_types,unnecessary_const,non_constant_identifier_names,library_prefixes,unused_import,unused_shown_name,return_of_invalid_type,unnecessary_this,prefer_final_fields + +import 'dart:core' as $core; + +import 'package:fixnum/fixnum.dart' as $fixnum; +import 'package:protobuf/protobuf.dart' as $pb; + +import 'package:protobuf/src/protobuf/mixins/well_known.dart' as $mixin; + +class Duration extends $pb.GeneratedMessage with $mixin.DurationMixin { + static final $pb.BuilderInfo _i = $pb.BuilderInfo( + const $core.bool.fromEnvironment('protobuf.omit_message_names') + ? '' + : 'Duration', + package: const $pb.PackageName( + const $core.bool.fromEnvironment('protobuf.omit_message_names') + ? '' + : 'google.protobuf'), + createEmptyInstance: create, + toProto3Json: $mixin.DurationMixin.toProto3JsonHelper, + fromProto3Json: $mixin.DurationMixin.fromProto3JsonHelper) + ..aInt64( + 1, + const $core.bool.fromEnvironment('protobuf.omit_field_names') + ? '' + : 'seconds') + ..a<$core.int>( + 2, + const $core.bool.fromEnvironment('protobuf.omit_field_names') + ? '' + : 'nanos', + $pb.PbFieldType.O3) + ..hasRequiredFields = false; + + Duration._() : super(); + factory Duration({ + $fixnum.Int64 seconds, + $core.int nanos, + }) { + final _result = create(); + if (seconds != null) { + _result.seconds = seconds; + } + if (nanos != null) { + _result.nanos = nanos; + } + return _result; + } + factory Duration.fromBuffer($core.List<$core.int> i, + [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromBuffer(i, r); + factory Duration.fromJson($core.String i, + [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromJson(i, r); + @$core.Deprecated('Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.deepCopy] instead. ' + 'Will be removed in next major version') + Duration clone() => Duration()..mergeFromMessage(this); + @$core.Deprecated('Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.rebuild] instead. ' + 'Will be removed in next major version') + Duration copyWith(void Function(Duration) updates) => + super.copyWith((message) => + updates(message as Duration)); // ignore: deprecated_member_use + $pb.BuilderInfo get info_ => _i; + @$core.pragma('dart2js:noInline') + static Duration create() => Duration._(); + Duration createEmptyInstance() => create(); + static $pb.PbList<Duration> createRepeated() => $pb.PbList<Duration>(); + @$core.pragma('dart2js:noInline') + static Duration getDefault() => + _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor<Duration>(create); + static Duration _defaultInstance; + + @$pb.TagNumber(1) + $fixnum.Int64 get seconds => $_getI64(0); + @$pb.TagNumber(1) + set seconds($fixnum.Int64 v) { + $_setInt64(0, v); + } + + @$pb.TagNumber(1) + $core.bool hasSeconds() => $_has(0); + @$pb.TagNumber(1) + void clearSeconds() => clearField(1); + + @$pb.TagNumber(2) + $core.int get nanos => $_getIZ(1); + @$pb.TagNumber(2) + set nanos($core.int v) { + $_setSignedInt32(1, v); + } + + @$pb.TagNumber(2) + $core.bool hasNanos() => $_has(1); + @$pb.TagNumber(2) + void clearNanos() => clearField(2); +}
diff --git a/grpc/example/googleapis/lib/src/generated/google/protobuf/duration.pbenum.dart b/grpc/example/googleapis/lib/src/generated/google/protobuf/duration.pbenum.dart new file mode 100644 index 0000000..a8c6583 --- /dev/null +++ b/grpc/example/googleapis/lib/src/generated/google/protobuf/duration.pbenum.dart
@@ -0,0 +1,6 @@ +/// +// Generated code. Do not modify. +// source: google/protobuf/duration.proto +// +// @dart = 2.7 +// ignore_for_file: annotate_overrides,camel_case_types,unnecessary_const,non_constant_identifier_names,library_prefixes,unused_import,unused_shown_name,return_of_invalid_type,unnecessary_this,prefer_final_fields
diff --git a/grpc/example/googleapis/lib/src/generated/google/protobuf/duration.pbjson.dart b/grpc/example/googleapis/lib/src/generated/google/protobuf/duration.pbjson.dart new file mode 100644 index 0000000..641c186 --- /dev/null +++ b/grpc/example/googleapis/lib/src/generated/google/protobuf/duration.pbjson.dart
@@ -0,0 +1,14 @@ +/// +// Generated code. Do not modify. +// source: google/protobuf/duration.proto +// +// @dart = 2.7 +// ignore_for_file: annotate_overrides,camel_case_types,unnecessary_const,non_constant_identifier_names,library_prefixes,unused_import,unused_shown_name,return_of_invalid_type,unnecessary_this,prefer_final_fields + +const Duration$json = const { + '1': 'Duration', + '2': const [ + const {'1': 'seconds', '3': 1, '4': 1, '5': 3, '10': 'seconds'}, + const {'1': 'nanos', '3': 2, '4': 1, '5': 5, '10': 'nanos'}, + ], +};
diff --git a/grpc/example/googleapis/lib/src/generated/google/protobuf/empty.pb.dart b/grpc/example/googleapis/lib/src/generated/google/protobuf/empty.pb.dart new file mode 100644 index 0000000..6d5b620 --- /dev/null +++ b/grpc/example/googleapis/lib/src/generated/google/protobuf/empty.pb.dart
@@ -0,0 +1,50 @@ +/// +// Generated code. Do not modify. +// source: google/protobuf/empty.proto +// +// @dart = 2.7 +// ignore_for_file: annotate_overrides,camel_case_types,unnecessary_const,non_constant_identifier_names,library_prefixes,unused_import,unused_shown_name,return_of_invalid_type,unnecessary_this,prefer_final_fields + +import 'dart:core' as $core; + +import 'package:protobuf/protobuf.dart' as $pb; + +class Empty extends $pb.GeneratedMessage { + static final $pb.BuilderInfo _i = $pb.BuilderInfo( + const $core.bool.fromEnvironment('protobuf.omit_message_names') + ? '' + : 'Empty', + package: const $pb.PackageName( + const $core.bool.fromEnvironment('protobuf.omit_message_names') + ? '' + : 'google.protobuf'), + createEmptyInstance: create) + ..hasRequiredFields = false; + + Empty._() : super(); + factory Empty() => create(); + factory Empty.fromBuffer($core.List<$core.int> i, + [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromBuffer(i, r); + factory Empty.fromJson($core.String i, + [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromJson(i, r); + @$core.Deprecated('Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.deepCopy] instead. ' + 'Will be removed in next major version') + Empty clone() => Empty()..mergeFromMessage(this); + @$core.Deprecated('Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.rebuild] instead. ' + 'Will be removed in next major version') + Empty copyWith(void Function(Empty) updates) => super.copyWith( + (message) => updates(message as Empty)); // ignore: deprecated_member_use + $pb.BuilderInfo get info_ => _i; + @$core.pragma('dart2js:noInline') + static Empty create() => Empty._(); + Empty createEmptyInstance() => create(); + static $pb.PbList<Empty> createRepeated() => $pb.PbList<Empty>(); + @$core.pragma('dart2js:noInline') + static Empty getDefault() => + _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor<Empty>(create); + static Empty _defaultInstance; +}
diff --git a/grpc/example/googleapis/lib/src/generated/google/protobuf/empty.pbenum.dart b/grpc/example/googleapis/lib/src/generated/google/protobuf/empty.pbenum.dart new file mode 100644 index 0000000..9246867 --- /dev/null +++ b/grpc/example/googleapis/lib/src/generated/google/protobuf/empty.pbenum.dart
@@ -0,0 +1,6 @@ +/// +// Generated code. Do not modify. +// source: google/protobuf/empty.proto +// +// @dart = 2.7 +// ignore_for_file: annotate_overrides,camel_case_types,unnecessary_const,non_constant_identifier_names,library_prefixes,unused_import,unused_shown_name,return_of_invalid_type,unnecessary_this,prefer_final_fields
diff --git a/grpc/example/googleapis/lib/src/generated/google/protobuf/empty.pbjson.dart b/grpc/example/googleapis/lib/src/generated/google/protobuf/empty.pbjson.dart new file mode 100644 index 0000000..3d558d3 --- /dev/null +++ b/grpc/example/googleapis/lib/src/generated/google/protobuf/empty.pbjson.dart
@@ -0,0 +1,10 @@ +/// +// Generated code. Do not modify. +// source: google/protobuf/empty.proto +// +// @dart = 2.7 +// ignore_for_file: annotate_overrides,camel_case_types,unnecessary_const,non_constant_identifier_names,library_prefixes,unused_import,unused_shown_name,return_of_invalid_type,unnecessary_this,prefer_final_fields + +const Empty$json = const { + '1': 'Empty', +};
diff --git a/grpc/example/googleapis/lib/src/generated/google/protobuf/struct.pb.dart b/grpc/example/googleapis/lib/src/generated/google/protobuf/struct.pb.dart new file mode 100644 index 0000000..6db1f79 --- /dev/null +++ b/grpc/example/googleapis/lib/src/generated/google/protobuf/struct.pb.dart
@@ -0,0 +1,346 @@ +/// +// Generated code. Do not modify. +// source: google/protobuf/struct.proto +// +// @dart = 2.7 +// ignore_for_file: annotate_overrides,camel_case_types,unnecessary_const,non_constant_identifier_names,library_prefixes,unused_import,unused_shown_name,return_of_invalid_type,unnecessary_this,prefer_final_fields + +import 'dart:core' as $core; + +import 'package:protobuf/protobuf.dart' as $pb; + +import 'package:protobuf/src/protobuf/mixins/well_known.dart' as $mixin; + +import 'struct.pbenum.dart'; + +export 'struct.pbenum.dart'; + +class Struct extends $pb.GeneratedMessage with $mixin.StructMixin { + static final $pb.BuilderInfo _i = $pb.BuilderInfo( + const $core.bool.fromEnvironment('protobuf.omit_message_names') + ? '' + : 'Struct', + package: const $pb.PackageName( + const $core.bool.fromEnvironment('protobuf.omit_message_names') + ? '' + : 'google.protobuf'), + createEmptyInstance: create, + toProto3Json: $mixin.StructMixin.toProto3JsonHelper, + fromProto3Json: $mixin.StructMixin.fromProto3JsonHelper) + ..m<$core.String, Value>( + 1, + const $core.bool.fromEnvironment('protobuf.omit_field_names') + ? '' + : 'fields', + entryClassName: 'Struct.FieldsEntry', + keyFieldType: $pb.PbFieldType.OS, + valueFieldType: $pb.PbFieldType.OM, + valueCreator: Value.create, + packageName: const $pb.PackageName('google.protobuf')) + ..hasRequiredFields = false; + + Struct._() : super(); + factory Struct({ + $core.Map<$core.String, Value> fields, + }) { + final _result = create(); + if (fields != null) { + _result.fields.addAll(fields); + } + return _result; + } + factory Struct.fromBuffer($core.List<$core.int> i, + [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromBuffer(i, r); + factory Struct.fromJson($core.String i, + [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromJson(i, r); + @$core.Deprecated('Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.deepCopy] instead. ' + 'Will be removed in next major version') + Struct clone() => Struct()..mergeFromMessage(this); + @$core.Deprecated('Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.rebuild] instead. ' + 'Will be removed in next major version') + Struct copyWith(void Function(Struct) updates) => super.copyWith( + (message) => updates(message as Struct)); // ignore: deprecated_member_use + $pb.BuilderInfo get info_ => _i; + @$core.pragma('dart2js:noInline') + static Struct create() => Struct._(); + Struct createEmptyInstance() => create(); + static $pb.PbList<Struct> createRepeated() => $pb.PbList<Struct>(); + @$core.pragma('dart2js:noInline') + static Struct getDefault() => + _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor<Struct>(create); + static Struct _defaultInstance; + + @$pb.TagNumber(1) + $core.Map<$core.String, Value> get fields => $_getMap(0); +} + +enum Value_Kind { + nullValue, + numberValue, + stringValue, + boolValue, + structValue, + listValue, + notSet +} + +class Value extends $pb.GeneratedMessage with $mixin.ValueMixin { + static const $core.Map<$core.int, Value_Kind> _Value_KindByTag = { + 1: Value_Kind.nullValue, + 2: Value_Kind.numberValue, + 3: Value_Kind.stringValue, + 4: Value_Kind.boolValue, + 5: Value_Kind.structValue, + 6: Value_Kind.listValue, + 0: Value_Kind.notSet + }; + static final $pb.BuilderInfo _i = $pb.BuilderInfo( + const $core.bool.fromEnvironment('protobuf.omit_message_names') + ? '' + : 'Value', + package: const $pb.PackageName( + const $core.bool.fromEnvironment('protobuf.omit_message_names') + ? '' + : 'google.protobuf'), + createEmptyInstance: create, + toProto3Json: $mixin.ValueMixin.toProto3JsonHelper, + fromProto3Json: $mixin.ValueMixin.fromProto3JsonHelper) + ..oo(0, [1, 2, 3, 4, 5, 6]) + ..e<NullValue>( + 1, + const $core.bool.fromEnvironment('protobuf.omit_field_names') + ? '' + : 'nullValue', + $pb.PbFieldType.OE, + defaultOrMaker: NullValue.NULL_VALUE, + valueOf: NullValue.valueOf, + enumValues: NullValue.values) + ..a<$core.double>( + 2, + const $core.bool.fromEnvironment('protobuf.omit_field_names') + ? '' + : 'numberValue', + $pb.PbFieldType.OD) + ..aOS( + 3, + const $core.bool.fromEnvironment('protobuf.omit_field_names') + ? '' + : 'stringValue') + ..aOB( + 4, + const $core.bool.fromEnvironment('protobuf.omit_field_names') + ? '' + : 'boolValue') + ..aOM<Struct>( + 5, + const $core.bool.fromEnvironment('protobuf.omit_field_names') + ? '' + : 'structValue', + subBuilder: Struct.create) + ..aOM<ListValue>( + 6, + const $core.bool.fromEnvironment('protobuf.omit_field_names') + ? '' + : 'listValue', + subBuilder: ListValue.create) + ..hasRequiredFields = false; + + Value._() : super(); + factory Value({ + NullValue nullValue, + $core.double numberValue, + $core.String stringValue, + $core.bool boolValue, + Struct structValue, + ListValue listValue, + }) { + final _result = create(); + if (nullValue != null) { + _result.nullValue = nullValue; + } + if (numberValue != null) { + _result.numberValue = numberValue; + } + if (stringValue != null) { + _result.stringValue = stringValue; + } + if (boolValue != null) { + _result.boolValue = boolValue; + } + if (structValue != null) { + _result.structValue = structValue; + } + if (listValue != null) { + _result.listValue = listValue; + } + return _result; + } + factory Value.fromBuffer($core.List<$core.int> i, + [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromBuffer(i, r); + factory Value.fromJson($core.String i, + [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromJson(i, r); + @$core.Deprecated('Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.deepCopy] instead. ' + 'Will be removed in next major version') + Value clone() => Value()..mergeFromMessage(this); + @$core.Deprecated('Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.rebuild] instead. ' + 'Will be removed in next major version') + Value copyWith(void Function(Value) updates) => super.copyWith( + (message) => updates(message as Value)); // ignore: deprecated_member_use + $pb.BuilderInfo get info_ => _i; + @$core.pragma('dart2js:noInline') + static Value create() => Value._(); + Value createEmptyInstance() => create(); + static $pb.PbList<Value> createRepeated() => $pb.PbList<Value>(); + @$core.pragma('dart2js:noInline') + static Value getDefault() => + _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor<Value>(create); + static Value _defaultInstance; + + Value_Kind whichKind() => _Value_KindByTag[$_whichOneof(0)]; + void clearKind() => clearField($_whichOneof(0)); + + @$pb.TagNumber(1) + NullValue get nullValue => $_getN(0); + @$pb.TagNumber(1) + set nullValue(NullValue v) { + setField(1, v); + } + + @$pb.TagNumber(1) + $core.bool hasNullValue() => $_has(0); + @$pb.TagNumber(1) + void clearNullValue() => clearField(1); + + @$pb.TagNumber(2) + $core.double get numberValue => $_getN(1); + @$pb.TagNumber(2) + set numberValue($core.double v) { + $_setDouble(1, v); + } + + @$pb.TagNumber(2) + $core.bool hasNumberValue() => $_has(1); + @$pb.TagNumber(2) + void clearNumberValue() => clearField(2); + + @$pb.TagNumber(3) + $core.String get stringValue => $_getSZ(2); + @$pb.TagNumber(3) + set stringValue($core.String v) { + $_setString(2, v); + } + + @$pb.TagNumber(3) + $core.bool hasStringValue() => $_has(2); + @$pb.TagNumber(3) + void clearStringValue() => clearField(3); + + @$pb.TagNumber(4) + $core.bool get boolValue => $_getBF(3); + @$pb.TagNumber(4) + set boolValue($core.bool v) { + $_setBool(3, v); + } + + @$pb.TagNumber(4) + $core.bool hasBoolValue() => $_has(3); + @$pb.TagNumber(4) + void clearBoolValue() => clearField(4); + + @$pb.TagNumber(5) + Struct get structValue => $_getN(4); + @$pb.TagNumber(5) + set structValue(Struct v) { + setField(5, v); + } + + @$pb.TagNumber(5) + $core.bool hasStructValue() => $_has(4); + @$pb.TagNumber(5) + void clearStructValue() => clearField(5); + @$pb.TagNumber(5) + Struct ensureStructValue() => $_ensure(4); + + @$pb.TagNumber(6) + ListValue get listValue => $_getN(5); + @$pb.TagNumber(6) + set listValue(ListValue v) { + setField(6, v); + } + + @$pb.TagNumber(6) + $core.bool hasListValue() => $_has(5); + @$pb.TagNumber(6) + void clearListValue() => clearField(6); + @$pb.TagNumber(6) + ListValue ensureListValue() => $_ensure(5); +} + +class ListValue extends $pb.GeneratedMessage with $mixin.ListValueMixin { + static final $pb.BuilderInfo _i = $pb.BuilderInfo( + const $core.bool.fromEnvironment('protobuf.omit_message_names') + ? '' + : 'ListValue', + package: const $pb.PackageName( + const $core.bool.fromEnvironment('protobuf.omit_message_names') + ? '' + : 'google.protobuf'), + createEmptyInstance: create, + toProto3Json: $mixin.ListValueMixin.toProto3JsonHelper, + fromProto3Json: $mixin.ListValueMixin.fromProto3JsonHelper) + ..pc<Value>( + 1, + const $core.bool.fromEnvironment('protobuf.omit_field_names') + ? '' + : 'values', + $pb.PbFieldType.PM, + subBuilder: Value.create) + ..hasRequiredFields = false; + + ListValue._() : super(); + factory ListValue({ + $core.Iterable<Value> values, + }) { + final _result = create(); + if (values != null) { + _result.values.addAll(values); + } + return _result; + } + factory ListValue.fromBuffer($core.List<$core.int> i, + [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromBuffer(i, r); + factory ListValue.fromJson($core.String i, + [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromJson(i, r); + @$core.Deprecated('Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.deepCopy] instead. ' + 'Will be removed in next major version') + ListValue clone() => ListValue()..mergeFromMessage(this); + @$core.Deprecated('Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.rebuild] instead. ' + 'Will be removed in next major version') + ListValue copyWith(void Function(ListValue) updates) => + super.copyWith((message) => + updates(message as ListValue)); // ignore: deprecated_member_use + $pb.BuilderInfo get info_ => _i; + @$core.pragma('dart2js:noInline') + static ListValue create() => ListValue._(); + ListValue createEmptyInstance() => create(); + static $pb.PbList<ListValue> createRepeated() => $pb.PbList<ListValue>(); + @$core.pragma('dart2js:noInline') + static ListValue getDefault() => + _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor<ListValue>(create); + static ListValue _defaultInstance; + + @$pb.TagNumber(1) + $core.List<Value> get values => $_getList(0); +}
diff --git a/grpc/example/googleapis/lib/src/generated/google/protobuf/struct.pbenum.dart b/grpc/example/googleapis/lib/src/generated/google/protobuf/struct.pbenum.dart new file mode 100644 index 0000000..b28644c --- /dev/null +++ b/grpc/example/googleapis/lib/src/generated/google/protobuf/struct.pbenum.dart
@@ -0,0 +1,28 @@ +/// +// Generated code. Do not modify. +// source: google/protobuf/struct.proto +// +// @dart = 2.7 +// ignore_for_file: annotate_overrides,camel_case_types,unnecessary_const,non_constant_identifier_names,library_prefixes,unused_import,unused_shown_name,return_of_invalid_type,unnecessary_this,prefer_final_fields + +// ignore_for_file: UNDEFINED_SHOWN_NAME +import 'dart:core' as $core; +import 'package:protobuf/protobuf.dart' as $pb; + +class NullValue extends $pb.ProtobufEnum { + static const NullValue NULL_VALUE = NullValue._( + 0, + const $core.bool.fromEnvironment('protobuf.omit_enum_names') + ? '' + : 'NULL_VALUE'); + + static const $core.List<NullValue> values = <NullValue>[ + NULL_VALUE, + ]; + + static final $core.Map<$core.int, NullValue> _byValue = + $pb.ProtobufEnum.initByValue(values); + static NullValue valueOf($core.int value) => _byValue[value]; + + const NullValue._($core.int v, $core.String n) : super(v, n); +}
diff --git a/grpc/example/googleapis/lib/src/generated/google/protobuf/struct.pbjson.dart b/grpc/example/googleapis/lib/src/generated/google/protobuf/struct.pbjson.dart new file mode 100644 index 0000000..29be569 --- /dev/null +++ b/grpc/example/googleapis/lib/src/generated/google/protobuf/struct.pbjson.dart
@@ -0,0 +1,118 @@ +/// +// Generated code. Do not modify. +// source: google/protobuf/struct.proto +// +// @dart = 2.7 +// ignore_for_file: annotate_overrides,camel_case_types,unnecessary_const,non_constant_identifier_names,library_prefixes,unused_import,unused_shown_name,return_of_invalid_type,unnecessary_this,prefer_final_fields + +const NullValue$json = const { + '1': 'NullValue', + '2': const [ + const {'1': 'NULL_VALUE', '2': 0}, + ], +}; + +const Struct$json = const { + '1': 'Struct', + '2': const [ + const { + '1': 'fields', + '3': 1, + '4': 3, + '5': 11, + '6': '.google.protobuf.Struct.FieldsEntry', + '10': 'fields' + }, + ], + '3': const [Struct_FieldsEntry$json], +}; + +const Struct_FieldsEntry$json = const { + '1': 'FieldsEntry', + '2': const [ + const {'1': 'key', '3': 1, '4': 1, '5': 9, '10': 'key'}, + const { + '1': 'value', + '3': 2, + '4': 1, + '5': 11, + '6': '.google.protobuf.Value', + '10': 'value' + }, + ], + '7': const {'7': true}, +}; + +const Value$json = const { + '1': 'Value', + '2': const [ + const { + '1': 'null_value', + '3': 1, + '4': 1, + '5': 14, + '6': '.google.protobuf.NullValue', + '9': 0, + '10': 'nullValue' + }, + const { + '1': 'number_value', + '3': 2, + '4': 1, + '5': 1, + '9': 0, + '10': 'numberValue' + }, + const { + '1': 'string_value', + '3': 3, + '4': 1, + '5': 9, + '9': 0, + '10': 'stringValue' + }, + const { + '1': 'bool_value', + '3': 4, + '4': 1, + '5': 8, + '9': 0, + '10': 'boolValue' + }, + const { + '1': 'struct_value', + '3': 5, + '4': 1, + '5': 11, + '6': '.google.protobuf.Struct', + '9': 0, + '10': 'structValue' + }, + const { + '1': 'list_value', + '3': 6, + '4': 1, + '5': 11, + '6': '.google.protobuf.ListValue', + '9': 0, + '10': 'listValue' + }, + ], + '8': const [ + const {'1': 'kind'}, + ], +}; + +const ListValue$json = const { + '1': 'ListValue', + '2': const [ + const { + '1': 'values', + '3': 1, + '4': 3, + '5': 11, + '6': '.google.protobuf.Value', + '10': 'values' + }, + ], +};
diff --git a/grpc/example/googleapis/lib/src/generated/google/protobuf/timestamp.pb.dart b/grpc/example/googleapis/lib/src/generated/google/protobuf/timestamp.pb.dart new file mode 100644 index 0000000..11fe236 --- /dev/null +++ b/grpc/example/googleapis/lib/src/generated/google/protobuf/timestamp.pb.dart
@@ -0,0 +1,112 @@ +/// +// Generated code. Do not modify. +// source: google/protobuf/timestamp.proto +// +// @dart = 2.7 +// ignore_for_file: annotate_overrides,camel_case_types,unnecessary_const,non_constant_identifier_names,library_prefixes,unused_import,unused_shown_name,return_of_invalid_type,unnecessary_this,prefer_final_fields + +import 'dart:core' as $core; + +import 'package:fixnum/fixnum.dart' as $fixnum; +import 'package:protobuf/protobuf.dart' as $pb; + +import 'package:protobuf/src/protobuf/mixins/well_known.dart' as $mixin; + +class Timestamp extends $pb.GeneratedMessage with $mixin.TimestampMixin { + static final $pb.BuilderInfo _i = $pb.BuilderInfo( + const $core.bool.fromEnvironment('protobuf.omit_message_names') + ? '' + : 'Timestamp', + package: const $pb.PackageName( + const $core.bool.fromEnvironment('protobuf.omit_message_names') + ? '' + : 'google.protobuf'), + createEmptyInstance: create, + toProto3Json: $mixin.TimestampMixin.toProto3JsonHelper, + fromProto3Json: $mixin.TimestampMixin.fromProto3JsonHelper) + ..aInt64( + 1, + const $core.bool.fromEnvironment('protobuf.omit_field_names') + ? '' + : 'seconds') + ..a<$core.int>( + 2, + const $core.bool.fromEnvironment('protobuf.omit_field_names') + ? '' + : 'nanos', + $pb.PbFieldType.O3) + ..hasRequiredFields = false; + + Timestamp._() : super(); + factory Timestamp({ + $fixnum.Int64 seconds, + $core.int nanos, + }) { + final _result = create(); + if (seconds != null) { + _result.seconds = seconds; + } + if (nanos != null) { + _result.nanos = nanos; + } + return _result; + } + factory Timestamp.fromBuffer($core.List<$core.int> i, + [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromBuffer(i, r); + factory Timestamp.fromJson($core.String i, + [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromJson(i, r); + @$core.Deprecated('Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.deepCopy] instead. ' + 'Will be removed in next major version') + Timestamp clone() => Timestamp()..mergeFromMessage(this); + @$core.Deprecated('Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.rebuild] instead. ' + 'Will be removed in next major version') + Timestamp copyWith(void Function(Timestamp) updates) => + super.copyWith((message) => + updates(message as Timestamp)); // ignore: deprecated_member_use + $pb.BuilderInfo get info_ => _i; + @$core.pragma('dart2js:noInline') + static Timestamp create() => Timestamp._(); + Timestamp createEmptyInstance() => create(); + static $pb.PbList<Timestamp> createRepeated() => $pb.PbList<Timestamp>(); + @$core.pragma('dart2js:noInline') + static Timestamp getDefault() => + _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor<Timestamp>(create); + static Timestamp _defaultInstance; + + @$pb.TagNumber(1) + $fixnum.Int64 get seconds => $_getI64(0); + @$pb.TagNumber(1) + set seconds($fixnum.Int64 v) { + $_setInt64(0, v); + } + + @$pb.TagNumber(1) + $core.bool hasSeconds() => $_has(0); + @$pb.TagNumber(1) + void clearSeconds() => clearField(1); + + @$pb.TagNumber(2) + $core.int get nanos => $_getIZ(1); + @$pb.TagNumber(2) + set nanos($core.int v) { + $_setSignedInt32(1, v); + } + + @$pb.TagNumber(2) + $core.bool hasNanos() => $_has(1); + @$pb.TagNumber(2) + void clearNanos() => clearField(2); + + /// Creates a new instance from [dateTime]. + /// + /// Time zone information will not be preserved. + static Timestamp fromDateTime($core.DateTime dateTime) { + final result = create(); + $mixin.TimestampMixin.setFromDateTime(result, dateTime); + return result; + } +}
diff --git a/grpc/example/googleapis/lib/src/generated/google/protobuf/timestamp.pbenum.dart b/grpc/example/googleapis/lib/src/generated/google/protobuf/timestamp.pbenum.dart new file mode 100644 index 0000000..262b661 --- /dev/null +++ b/grpc/example/googleapis/lib/src/generated/google/protobuf/timestamp.pbenum.dart
@@ -0,0 +1,6 @@ +/// +// Generated code. Do not modify. +// source: google/protobuf/timestamp.proto +// +// @dart = 2.7 +// ignore_for_file: annotate_overrides,camel_case_types,unnecessary_const,non_constant_identifier_names,library_prefixes,unused_import,unused_shown_name,return_of_invalid_type,unnecessary_this,prefer_final_fields
diff --git a/grpc/example/googleapis/lib/src/generated/google/protobuf/timestamp.pbjson.dart b/grpc/example/googleapis/lib/src/generated/google/protobuf/timestamp.pbjson.dart new file mode 100644 index 0000000..33c7a8a --- /dev/null +++ b/grpc/example/googleapis/lib/src/generated/google/protobuf/timestamp.pbjson.dart
@@ -0,0 +1,14 @@ +/// +// Generated code. Do not modify. +// source: google/protobuf/timestamp.proto +// +// @dart = 2.7 +// ignore_for_file: annotate_overrides,camel_case_types,unnecessary_const,non_constant_identifier_names,library_prefixes,unused_import,unused_shown_name,return_of_invalid_type,unnecessary_this,prefer_final_fields + +const Timestamp$json = const { + '1': 'Timestamp', + '2': const [ + const {'1': 'seconds', '3': 1, '4': 1, '5': 3, '10': 'seconds'}, + const {'1': 'nanos', '3': 2, '4': 1, '5': 5, '10': 'nanos'}, + ], +};
diff --git a/grpc/example/googleapis/lib/src/generated/google/rpc/status.pb.dart b/grpc/example/googleapis/lib/src/generated/google/rpc/status.pb.dart new file mode 100644 index 0000000..21441ba --- /dev/null +++ b/grpc/example/googleapis/lib/src/generated/google/rpc/status.pb.dart
@@ -0,0 +1,113 @@ +/// +// Generated code. Do not modify. +// source: google/rpc/status.proto +// +// @dart = 2.7 +// ignore_for_file: annotate_overrides,camel_case_types,unnecessary_const,non_constant_identifier_names,library_prefixes,unused_import,unused_shown_name,return_of_invalid_type,unnecessary_this,prefer_final_fields + +import 'dart:core' as $core; + +import 'package:protobuf/protobuf.dart' as $pb; + +import '../protobuf/any.pb.dart' as $0; + +class Status extends $pb.GeneratedMessage { + static final $pb.BuilderInfo _i = $pb.BuilderInfo( + const $core.bool.fromEnvironment('protobuf.omit_message_names') + ? '' + : 'Status', + package: const $pb.PackageName( + const $core.bool.fromEnvironment('protobuf.omit_message_names') + ? '' + : 'google.rpc'), + createEmptyInstance: create) + ..a<$core.int>( + 1, + const $core.bool.fromEnvironment('protobuf.omit_field_names') + ? '' + : 'code', + $pb.PbFieldType.O3) + ..aOS( + 2, + const $core.bool.fromEnvironment('protobuf.omit_field_names') + ? '' + : 'message') + ..pc<$0.Any>( + 3, + const $core.bool.fromEnvironment('protobuf.omit_field_names') + ? '' + : 'details', + $pb.PbFieldType.PM, + subBuilder: $0.Any.create) + ..hasRequiredFields = false; + + Status._() : super(); + factory Status({ + $core.int code, + $core.String message, + $core.Iterable<$0.Any> details, + }) { + final _result = create(); + if (code != null) { + _result.code = code; + } + if (message != null) { + _result.message = message; + } + if (details != null) { + _result.details.addAll(details); + } + return _result; + } + factory Status.fromBuffer($core.List<$core.int> i, + [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromBuffer(i, r); + factory Status.fromJson($core.String i, + [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromJson(i, r); + @$core.Deprecated('Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.deepCopy] instead. ' + 'Will be removed in next major version') + Status clone() => Status()..mergeFromMessage(this); + @$core.Deprecated('Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.rebuild] instead. ' + 'Will be removed in next major version') + Status copyWith(void Function(Status) updates) => super.copyWith( + (message) => updates(message as Status)); // ignore: deprecated_member_use + $pb.BuilderInfo get info_ => _i; + @$core.pragma('dart2js:noInline') + static Status create() => Status._(); + Status createEmptyInstance() => create(); + static $pb.PbList<Status> createRepeated() => $pb.PbList<Status>(); + @$core.pragma('dart2js:noInline') + static Status getDefault() => + _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor<Status>(create); + static Status _defaultInstance; + + @$pb.TagNumber(1) + $core.int get code => $_getIZ(0); + @$pb.TagNumber(1) + set code($core.int v) { + $_setSignedInt32(0, v); + } + + @$pb.TagNumber(1) + $core.bool hasCode() => $_has(0); + @$pb.TagNumber(1) + void clearCode() => clearField(1); + + @$pb.TagNumber(2) + $core.String get message => $_getSZ(1); + @$pb.TagNumber(2) + set message($core.String v) { + $_setString(1, v); + } + + @$pb.TagNumber(2) + $core.bool hasMessage() => $_has(1); + @$pb.TagNumber(2) + void clearMessage() => clearField(2); + + @$pb.TagNumber(3) + $core.List<$0.Any> get details => $_getList(2); +}
diff --git a/grpc/example/googleapis/lib/src/generated/google/rpc/status.pbenum.dart b/grpc/example/googleapis/lib/src/generated/google/rpc/status.pbenum.dart new file mode 100644 index 0000000..a1f6810 --- /dev/null +++ b/grpc/example/googleapis/lib/src/generated/google/rpc/status.pbenum.dart
@@ -0,0 +1,6 @@ +/// +// Generated code. Do not modify. +// source: google/rpc/status.proto +// +// @dart = 2.7 +// ignore_for_file: annotate_overrides,camel_case_types,unnecessary_const,non_constant_identifier_names,library_prefixes,unused_import,unused_shown_name,return_of_invalid_type,unnecessary_this,prefer_final_fields
diff --git a/grpc/example/googleapis/lib/src/generated/google/rpc/status.pbjson.dart b/grpc/example/googleapis/lib/src/generated/google/rpc/status.pbjson.dart new file mode 100644 index 0000000..240b514 --- /dev/null +++ b/grpc/example/googleapis/lib/src/generated/google/rpc/status.pbjson.dart
@@ -0,0 +1,22 @@ +/// +// Generated code. Do not modify. +// source: google/rpc/status.proto +// +// @dart = 2.7 +// ignore_for_file: annotate_overrides,camel_case_types,unnecessary_const,non_constant_identifier_names,library_prefixes,unused_import,unused_shown_name,return_of_invalid_type,unnecessary_this,prefer_final_fields + +const Status$json = const { + '1': 'Status', + '2': const [ + const {'1': 'code', '3': 1, '4': 1, '5': 5, '10': 'code'}, + const {'1': 'message', '3': 2, '4': 1, '5': 9, '10': 'message'}, + const { + '1': 'details', + '3': 3, + '4': 3, + '5': 11, + '6': '.google.protobuf.Any', + '10': 'details' + }, + ], +};
diff --git a/grpc/example/googleapis/pubspec.yaml b/grpc/example/googleapis/pubspec.yaml new file mode 100644 index 0000000..06d894c --- /dev/null +++ b/grpc/example/googleapis/pubspec.yaml
@@ -0,0 +1,15 @@ +name: googleapis +description: Dart gRPC client sample for Google APIs +publish_to: none + +environment: + sdk: '>=2.11.99 <3.0.0' + +dependencies: + async: ^2.2.0 + grpc: + path: ../../ + protobuf: ^2.0.0 + +dev_dependencies: + test: ^1.6.4
diff --git a/grpc/example/googleapis/tool/regenerate.sh b/grpc/example/googleapis/tool/regenerate.sh new file mode 100755 index 0000000..2cd3e02 --- /dev/null +++ b/grpc/example/googleapis/tool/regenerate.sh
@@ -0,0 +1,32 @@ +#!/usr/bin/env bash + +if [ ! -d "$PROTOBUF" ]; then + echo "Please set the PROTOBUF environment variable to your clone of google/protobuf." + exit -1 +fi + +if [ ! -d "$GOOGLEAPIS" ]; then + echo "Please set the GOOGLEAPIS environment variable to your clone of googleapis/googleapis." + exit -1 +fi + +PROTOC="protoc --dart_out=grpc:lib/src/generated -I$PROTOBUF/src -I$GOOGLEAPIS" + +$PROTOC $GOOGLEAPIS/google/logging/v2/logging.proto +$PROTOC $GOOGLEAPIS/google/logging/v2/log_entry.proto +$PROTOC $GOOGLEAPIS/google/logging/type/log_severity.proto +$PROTOC $GOOGLEAPIS/google/logging/type/http_request.proto + +$PROTOC $GOOGLEAPIS/google/api/monitored_resource.proto +$PROTOC $GOOGLEAPIS/google/api/label.proto +$PROTOC $GOOGLEAPIS/google/api/launch_stage.proto + +$PROTOC $GOOGLEAPIS/google/rpc/status.proto + +$PROTOC $PROTOBUF/src/google/protobuf/any.proto +$PROTOC $PROTOBUF/src/google/protobuf/duration.proto +$PROTOC $PROTOBUF/src/google/protobuf/empty.proto +$PROTOC $PROTOBUF/src/google/protobuf/struct.proto +$PROTOC $PROTOBUF/src/google/protobuf/timestamp.proto + +dartfmt -w lib/src/generated
diff --git a/grpc/example/grpc-web/README.md b/grpc/example/grpc-web/README.md new file mode 100644 index 0000000..5e8ce97 --- /dev/null +++ b/grpc/example/grpc-web/README.md
@@ -0,0 +1,63 @@ +# Description +The grpc-web example shows how to use the Dart gRPC library with a gRPC-Web capable server. + +This is meant to be used with the echo example provided by the grpc-web repository. The definition of the service is given in echo.proto. + +# Prerequisites + +Install 'webdev', by running + +```sh +$ pub global activate webdev +``` + +You will need a clone of the [grpc-web](https://github.com/grpc/grpc-web) repository to run the example server. + +# Run the sample code +Follow the instructions for starting the grpc-web example server. The simplest version of this involves running the grpc-web server in a docker container with: + +```sh +$ docker-compose up node-server envoy commonjs-client +``` + +To compile and run the example, assuming you are in the root of the grpc-web +folder, i.e., .../example/grpc-web/, first get the dependencies by running: + +```sh +$ pub get +``` + +Compile and run the website with: + +```sh +$ webdev serve web:9000 +``` + +Note that the alternate port (9000) is necessary because the grpc-web server runs the grpc server on port 8080 by default (the same as webdev). + +You can then navigate to http://localhost:9000/ to try out the example. + +# Regenerate the stubs + +If you have made changes to the message or service definition in +`protos/echo.proto` and need to regenerate the corresponding Dart files, +you will need to have protoc version 3.0.0 or higher and the Dart protoc plugin +version 16.0.0 or higher on your PATH. + +To install protoc, see the instructions on +[the Protocol Buffers website](https://developers.google.com/protocol-buffers/). + +The easiest way to get the Dart protoc plugin is by running + +```sh +$ pub global activate protoc_plugin +``` + +and follow the directions to add `~/.pub-cache/bin` to your PATH, if you haven't +already done so. + +You can now regenerate the Dart files by running + +```sh +$ protoc --dart_out=grpc:lib/src/generated -Iprotos protos/echo.proto +```
diff --git a/grpc/example/grpc-web/lib/app.dart b/grpc/example/grpc-web/lib/app.dart new file mode 100644 index 0000000..85ef7ab --- /dev/null +++ b/grpc/example/grpc-web/lib/app.dart
@@ -0,0 +1,53 @@ +import 'dart:async'; +import 'dart:html'; + +import 'package:grpc_web/src/generated/echo.pbgrpc.dart'; + +class EchoApp { + final EchoServiceClient _service; + + EchoApp(this._service); + + Future<void> echo(String message) async { + _addLeftMessage(message); + + try { + final response = await _service.echo(EchoRequest()..message = message); + _addRightMessage(response.message); + } catch (error) { + _addRightMessage(error.toString()); + } + } + + void repeatEcho(String message, int count) { + _addLeftMessage(message); + final request = ServerStreamingEchoRequest() + ..message = message + ..messageCount = count + ..messageInterval = 500; + _service.serverStreamingEcho(request).listen((response) { + _addRightMessage(response.message); + }, onError: (error) { + _addRightMessage(error.toString()); + }, onDone: () => print('Closed connection to server.')); + } + + void _addLeftMessage(String message) { + _addMessage(message, 'label-primary pull-left'); + } + + void _addRightMessage(String message) { + _addMessage(message, 'label-default pull-right'); + } + + void _addMessage(String message, String cssClass) { + final classes = cssClass.split(' '); + querySelector('#first').after(DivElement() + ..classes.add('row') + ..append(Element.tag('h2') + ..append(SpanElement() + ..classes.add('label') + ..classes.addAll(classes) + ..text = message))); + } +}
diff --git a/grpc/example/grpc-web/lib/src/generated/echo.pb.dart b/grpc/example/grpc-web/lib/src/generated/echo.pb.dart new file mode 100644 index 0000000..ac7c7ce --- /dev/null +++ b/grpc/example/grpc-web/lib/src/generated/echo.pb.dart
@@ -0,0 +1,326 @@ +/// +// Generated code. Do not modify. +// source: echo.proto +// +// @dart = 2.7 +// ignore_for_file: annotate_overrides,camel_case_types,unnecessary_const,non_constant_identifier_names,library_prefixes,unused_import,unused_shown_name,return_of_invalid_type,unnecessary_this,prefer_final_fields + +import 'dart:core' as $core; + +import 'package:protobuf/protobuf.dart' as $pb; + +class EchoRequest extends $pb.GeneratedMessage { + static final $pb.BuilderInfo _i = $pb.BuilderInfo( + const $core.bool.fromEnvironment('protobuf.omit_message_names') + ? '' + : 'EchoRequest', + package: const $pb.PackageName( + const $core.bool.fromEnvironment('protobuf.omit_message_names') + ? '' + : 'grpc.gateway.testing'), + createEmptyInstance: create) + ..aOS( + 1, + const $core.bool.fromEnvironment('protobuf.omit_field_names') + ? '' + : 'message') + ..hasRequiredFields = false; + + EchoRequest._() : super(); + factory EchoRequest({ + $core.String message, + }) { + final _result = create(); + if (message != null) { + _result.message = message; + } + return _result; + } + factory EchoRequest.fromBuffer($core.List<$core.int> i, + [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromBuffer(i, r); + factory EchoRequest.fromJson($core.String i, + [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromJson(i, r); + @$core.Deprecated('Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.deepCopy] instead. ' + 'Will be removed in next major version') + EchoRequest clone() => EchoRequest()..mergeFromMessage(this); + @$core.Deprecated('Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.rebuild] instead. ' + 'Will be removed in next major version') + EchoRequest copyWith(void Function(EchoRequest) updates) => + super.copyWith((message) => + updates(message as EchoRequest)); // ignore: deprecated_member_use + $pb.BuilderInfo get info_ => _i; + @$core.pragma('dart2js:noInline') + static EchoRequest create() => EchoRequest._(); + EchoRequest createEmptyInstance() => create(); + static $pb.PbList<EchoRequest> createRepeated() => $pb.PbList<EchoRequest>(); + @$core.pragma('dart2js:noInline') + static EchoRequest getDefault() => _defaultInstance ??= + $pb.GeneratedMessage.$_defaultFor<EchoRequest>(create); + static EchoRequest _defaultInstance; + + @$pb.TagNumber(1) + $core.String get message => $_getSZ(0); + @$pb.TagNumber(1) + set message($core.String v) { + $_setString(0, v); + } + + @$pb.TagNumber(1) + $core.bool hasMessage() => $_has(0); + @$pb.TagNumber(1) + void clearMessage() => clearField(1); +} + +class EchoResponse extends $pb.GeneratedMessage { + static final $pb.BuilderInfo _i = $pb.BuilderInfo( + const $core.bool.fromEnvironment('protobuf.omit_message_names') + ? '' + : 'EchoResponse', + package: const $pb.PackageName( + const $core.bool.fromEnvironment('protobuf.omit_message_names') + ? '' + : 'grpc.gateway.testing'), + createEmptyInstance: create) + ..aOS( + 1, + const $core.bool.fromEnvironment('protobuf.omit_field_names') + ? '' + : 'message') + ..hasRequiredFields = false; + + EchoResponse._() : super(); + factory EchoResponse({ + $core.String message, + }) { + final _result = create(); + if (message != null) { + _result.message = message; + } + return _result; + } + factory EchoResponse.fromBuffer($core.List<$core.int> i, + [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromBuffer(i, r); + factory EchoResponse.fromJson($core.String i, + [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromJson(i, r); + @$core.Deprecated('Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.deepCopy] instead. ' + 'Will be removed in next major version') + EchoResponse clone() => EchoResponse()..mergeFromMessage(this); + @$core.Deprecated('Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.rebuild] instead. ' + 'Will be removed in next major version') + EchoResponse copyWith(void Function(EchoResponse) updates) => + super.copyWith((message) => + updates(message as EchoResponse)); // ignore: deprecated_member_use + $pb.BuilderInfo get info_ => _i; + @$core.pragma('dart2js:noInline') + static EchoResponse create() => EchoResponse._(); + EchoResponse createEmptyInstance() => create(); + static $pb.PbList<EchoResponse> createRepeated() => + $pb.PbList<EchoResponse>(); + @$core.pragma('dart2js:noInline') + static EchoResponse getDefault() => _defaultInstance ??= + $pb.GeneratedMessage.$_defaultFor<EchoResponse>(create); + static EchoResponse _defaultInstance; + + @$pb.TagNumber(1) + $core.String get message => $_getSZ(0); + @$pb.TagNumber(1) + set message($core.String v) { + $_setString(0, v); + } + + @$pb.TagNumber(1) + $core.bool hasMessage() => $_has(0); + @$pb.TagNumber(1) + void clearMessage() => clearField(1); +} + +class ServerStreamingEchoRequest extends $pb.GeneratedMessage { + static final $pb.BuilderInfo _i = $pb.BuilderInfo( + const $core.bool.fromEnvironment('protobuf.omit_message_names') + ? '' + : 'ServerStreamingEchoRequest', + package: const $pb.PackageName( + const $core.bool.fromEnvironment('protobuf.omit_message_names') + ? '' + : 'grpc.gateway.testing'), + createEmptyInstance: create) + ..aOS( + 1, + const $core.bool.fromEnvironment('protobuf.omit_field_names') + ? '' + : 'message') + ..a<$core.int>( + 2, + const $core.bool.fromEnvironment('protobuf.omit_field_names') + ? '' + : 'messageCount', + $pb.PbFieldType.O3) + ..a<$core.int>( + 3, + const $core.bool.fromEnvironment('protobuf.omit_field_names') + ? '' + : 'messageInterval', + $pb.PbFieldType.O3) + ..hasRequiredFields = false; + + ServerStreamingEchoRequest._() : super(); + factory ServerStreamingEchoRequest({ + $core.String message, + $core.int messageCount, + $core.int messageInterval, + }) { + final _result = create(); + if (message != null) { + _result.message = message; + } + if (messageCount != null) { + _result.messageCount = messageCount; + } + if (messageInterval != null) { + _result.messageInterval = messageInterval; + } + return _result; + } + factory ServerStreamingEchoRequest.fromBuffer($core.List<$core.int> i, + [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromBuffer(i, r); + factory ServerStreamingEchoRequest.fromJson($core.String i, + [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromJson(i, r); + @$core.Deprecated('Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.deepCopy] instead. ' + 'Will be removed in next major version') + ServerStreamingEchoRequest clone() => + ServerStreamingEchoRequest()..mergeFromMessage(this); + @$core.Deprecated('Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.rebuild] instead. ' + 'Will be removed in next major version') + ServerStreamingEchoRequest copyWith( + void Function(ServerStreamingEchoRequest) updates) => + super.copyWith((message) => updates(message + as ServerStreamingEchoRequest)); // ignore: deprecated_member_use + $pb.BuilderInfo get info_ => _i; + @$core.pragma('dart2js:noInline') + static ServerStreamingEchoRequest create() => ServerStreamingEchoRequest._(); + ServerStreamingEchoRequest createEmptyInstance() => create(); + static $pb.PbList<ServerStreamingEchoRequest> createRepeated() => + $pb.PbList<ServerStreamingEchoRequest>(); + @$core.pragma('dart2js:noInline') + static ServerStreamingEchoRequest getDefault() => _defaultInstance ??= + $pb.GeneratedMessage.$_defaultFor<ServerStreamingEchoRequest>(create); + static ServerStreamingEchoRequest _defaultInstance; + + @$pb.TagNumber(1) + $core.String get message => $_getSZ(0); + @$pb.TagNumber(1) + set message($core.String v) { + $_setString(0, v); + } + + @$pb.TagNumber(1) + $core.bool hasMessage() => $_has(0); + @$pb.TagNumber(1) + void clearMessage() => clearField(1); + + @$pb.TagNumber(2) + $core.int get messageCount => $_getIZ(1); + @$pb.TagNumber(2) + set messageCount($core.int v) { + $_setSignedInt32(1, v); + } + + @$pb.TagNumber(2) + $core.bool hasMessageCount() => $_has(1); + @$pb.TagNumber(2) + void clearMessageCount() => clearField(2); + + @$pb.TagNumber(3) + $core.int get messageInterval => $_getIZ(2); + @$pb.TagNumber(3) + set messageInterval($core.int v) { + $_setSignedInt32(2, v); + } + + @$pb.TagNumber(3) + $core.bool hasMessageInterval() => $_has(2); + @$pb.TagNumber(3) + void clearMessageInterval() => clearField(3); +} + +class ServerStreamingEchoResponse extends $pb.GeneratedMessage { + static final $pb.BuilderInfo _i = $pb.BuilderInfo( + const $core.bool.fromEnvironment('protobuf.omit_message_names') + ? '' + : 'ServerStreamingEchoResponse', + package: const $pb.PackageName( + const $core.bool.fromEnvironment('protobuf.omit_message_names') + ? '' + : 'grpc.gateway.testing'), + createEmptyInstance: create) + ..aOS( + 1, + const $core.bool.fromEnvironment('protobuf.omit_field_names') + ? '' + : 'message') + ..hasRequiredFields = false; + + ServerStreamingEchoResponse._() : super(); + factory ServerStreamingEchoResponse({ + $core.String message, + }) { + final _result = create(); + if (message != null) { + _result.message = message; + } + return _result; + } + factory ServerStreamingEchoResponse.fromBuffer($core.List<$core.int> i, + [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromBuffer(i, r); + factory ServerStreamingEchoResponse.fromJson($core.String i, + [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromJson(i, r); + @$core.Deprecated('Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.deepCopy] instead. ' + 'Will be removed in next major version') + ServerStreamingEchoResponse clone() => + ServerStreamingEchoResponse()..mergeFromMessage(this); + @$core.Deprecated('Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.rebuild] instead. ' + 'Will be removed in next major version') + ServerStreamingEchoResponse copyWith( + void Function(ServerStreamingEchoResponse) updates) => + super.copyWith((message) => updates(message + as ServerStreamingEchoResponse)); // ignore: deprecated_member_use + $pb.BuilderInfo get info_ => _i; + @$core.pragma('dart2js:noInline') + static ServerStreamingEchoResponse create() => + ServerStreamingEchoResponse._(); + ServerStreamingEchoResponse createEmptyInstance() => create(); + static $pb.PbList<ServerStreamingEchoResponse> createRepeated() => + $pb.PbList<ServerStreamingEchoResponse>(); + @$core.pragma('dart2js:noInline') + static ServerStreamingEchoResponse getDefault() => _defaultInstance ??= + $pb.GeneratedMessage.$_defaultFor<ServerStreamingEchoResponse>(create); + static ServerStreamingEchoResponse _defaultInstance; + + @$pb.TagNumber(1) + $core.String get message => $_getSZ(0); + @$pb.TagNumber(1) + set message($core.String v) { + $_setString(0, v); + } + + @$pb.TagNumber(1) + $core.bool hasMessage() => $_has(0); + @$pb.TagNumber(1) + void clearMessage() => clearField(1); +}
diff --git a/grpc/example/grpc-web/lib/src/generated/echo.pbenum.dart b/grpc/example/grpc-web/lib/src/generated/echo.pbenum.dart new file mode 100644 index 0000000..a76f22d --- /dev/null +++ b/grpc/example/grpc-web/lib/src/generated/echo.pbenum.dart
@@ -0,0 +1,6 @@ +/// +// Generated code. Do not modify. +// source: echo.proto +// +// @dart = 2.7 +// ignore_for_file: annotate_overrides,camel_case_types,unnecessary_const,non_constant_identifier_names,library_prefixes,unused_import,unused_shown_name,return_of_invalid_type,unnecessary_this,prefer_final_fields
diff --git a/grpc/example/grpc-web/lib/src/generated/echo.pbgrpc.dart b/grpc/example/grpc-web/lib/src/generated/echo.pbgrpc.dart new file mode 100644 index 0000000..539c7d1 --- /dev/null +++ b/grpc/example/grpc-web/lib/src/generated/echo.pbgrpc.dart
@@ -0,0 +1,84 @@ +/// +// Generated code. Do not modify. +// source: echo.proto +// +// @dart = 2.7 +// ignore_for_file: annotate_overrides,camel_case_types,unnecessary_const,non_constant_identifier_names,library_prefixes,unused_import,unused_shown_name,return_of_invalid_type,unnecessary_this,prefer_final_fields + +import 'dart:async' as $async; + +import 'dart:core' as $core; + +import 'package:grpc/service_api.dart' as $grpc; +import 'echo.pb.dart' as $0; +export 'echo.pb.dart'; + +class EchoServiceClient extends $grpc.Client { + static final _$echo = $grpc.ClientMethod<$0.EchoRequest, $0.EchoResponse>( + '/grpc.gateway.testing.EchoService/Echo', + ($0.EchoRequest value) => value.writeToBuffer(), + ($core.List<$core.int> value) => $0.EchoResponse.fromBuffer(value)); + static final _$serverStreamingEcho = $grpc.ClientMethod< + $0.ServerStreamingEchoRequest, $0.ServerStreamingEchoResponse>( + '/grpc.gateway.testing.EchoService/ServerStreamingEcho', + ($0.ServerStreamingEchoRequest value) => value.writeToBuffer(), + ($core.List<$core.int> value) => + $0.ServerStreamingEchoResponse.fromBuffer(value)); + + EchoServiceClient($grpc.ClientChannel channel, + {$grpc.CallOptions options, + $core.Iterable<$grpc.ClientInterceptor> interceptors}) + : super(channel, options: options, interceptors: interceptors); + + $grpc.ResponseFuture<$0.EchoResponse> echo($0.EchoRequest request, + {$grpc.CallOptions options}) { + return $createUnaryCall(_$echo, request, options: options); + } + + $grpc.ResponseStream<$0.ServerStreamingEchoResponse> serverStreamingEcho( + $0.ServerStreamingEchoRequest request, + {$grpc.CallOptions options}) { + return $createStreamingCall( + _$serverStreamingEcho, $async.Stream.fromIterable([request]), + options: options); + } +} + +abstract class EchoServiceBase extends $grpc.Service { + $core.String get $name => 'grpc.gateway.testing.EchoService'; + + EchoServiceBase() { + $addMethod($grpc.ServiceMethod<$0.EchoRequest, $0.EchoResponse>( + 'Echo', + echo_Pre, + false, + false, + ($core.List<$core.int> value) => $0.EchoRequest.fromBuffer(value), + ($0.EchoResponse value) => value.writeToBuffer())); + $addMethod($grpc.ServiceMethod<$0.ServerStreamingEchoRequest, + $0.ServerStreamingEchoResponse>( + 'ServerStreamingEcho', + serverStreamingEcho_Pre, + false, + true, + ($core.List<$core.int> value) => + $0.ServerStreamingEchoRequest.fromBuffer(value), + ($0.ServerStreamingEchoResponse value) => value.writeToBuffer())); + } + + $async.Future<$0.EchoResponse> echo_Pre( + $grpc.ServiceCall call, $async.Future<$0.EchoRequest> request) async { + return echo(call, await request); + } + + $async.Stream<$0.ServerStreamingEchoResponse> serverStreamingEcho_Pre( + $grpc.ServiceCall call, + $async.Future<$0.ServerStreamingEchoRequest> request) async* { + yield* serverStreamingEcho(call, await request); + } + + $async.Future<$0.EchoResponse> echo( + $grpc.ServiceCall call, $0.EchoRequest request); + $async.Stream<$0.ServerStreamingEchoResponse> serverStreamingEcho( + $grpc.ServiceCall call, $0.ServerStreamingEchoRequest request); +}
diff --git a/grpc/example/grpc-web/lib/src/generated/echo.pbjson.dart b/grpc/example/grpc-web/lib/src/generated/echo.pbjson.dart new file mode 100644 index 0000000..a76ee14 --- /dev/null +++ b/grpc/example/grpc-web/lib/src/generated/echo.pbjson.dart
@@ -0,0 +1,42 @@ +/// +// Generated code. Do not modify. +// source: echo.proto +// +// @dart = 2.7 +// ignore_for_file: annotate_overrides,camel_case_types,unnecessary_const,non_constant_identifier_names,library_prefixes,unused_import,unused_shown_name,return_of_invalid_type,unnecessary_this,prefer_final_fields + +const EchoRequest$json = const { + '1': 'EchoRequest', + '2': const [ + const {'1': 'message', '3': 1, '4': 1, '5': 9, '10': 'message'}, + ], +}; + +const EchoResponse$json = const { + '1': 'EchoResponse', + '2': const [ + const {'1': 'message', '3': 1, '4': 1, '5': 9, '10': 'message'}, + ], +}; + +const ServerStreamingEchoRequest$json = const { + '1': 'ServerStreamingEchoRequest', + '2': const [ + const {'1': 'message', '3': 1, '4': 1, '5': 9, '10': 'message'}, + const {'1': 'message_count', '3': 2, '4': 1, '5': 5, '10': 'messageCount'}, + const { + '1': 'message_interval', + '3': 3, + '4': 1, + '5': 5, + '10': 'messageInterval' + }, + ], +}; + +const ServerStreamingEchoResponse$json = const { + '1': 'ServerStreamingEchoResponse', + '2': const [ + const {'1': 'message', '3': 1, '4': 1, '5': 9, '10': 'message'}, + ], +};
diff --git a/grpc/example/grpc-web/protos/echo.proto b/grpc/example/grpc-web/protos/echo.proto new file mode 100644 index 0000000..80a9507 --- /dev/null +++ b/grpc/example/grpc-web/protos/echo.proto
@@ -0,0 +1,41 @@ +// Copyright 2018 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +syntax = "proto3"; + +package grpc.gateway.testing; + +message EchoRequest { + string message = 1; +} + +message EchoResponse { + string message = 1; +} + +message ServerStreamingEchoRequest { + string message = 1; + int32 message_count = 2; + int32 message_interval = 3; +} + +message ServerStreamingEchoResponse { + string message = 1; +} + +service EchoService { + rpc Echo(EchoRequest) returns (EchoResponse); + rpc ServerStreamingEcho(ServerStreamingEchoRequest) + returns (stream ServerStreamingEchoResponse); +}
diff --git a/grpc/example/grpc-web/pubspec.yaml b/grpc/example/grpc-web/pubspec.yaml new file mode 100644 index 0000000..6e3efcd --- /dev/null +++ b/grpc/example/grpc-web/pubspec.yaml
@@ -0,0 +1,15 @@ +name: grpc_web +description: Dart gRPC-Web sample client +publish_to: none + +environment: + sdk: '>=2.11.99 <3.0.0' + +dependencies: + grpc: + path: ../../ + protobuf: ^2.0.0 + +dev_dependencies: + build_runner: ^2.0.0 + build_web_compilers: ^3.0.0
diff --git a/grpc/example/grpc-web/web/main.dart b/grpc/example/grpc-web/web/main.dart new file mode 100644 index 0000000..04d6154 --- /dev/null +++ b/grpc/example/grpc-web/web/main.dart
@@ -0,0 +1,47 @@ +// Copyright (c) 2018, the gRPC project authors. Please see the AUTHORS file +// for details. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +import 'dart:html'; + +import 'package:grpc/grpc_web.dart'; +import 'package:grpc_web/app.dart'; +import 'package:grpc_web/src/generated/echo.pbgrpc.dart'; + +void main() { + final channel = GrpcWebClientChannel.xhr(Uri.parse('http://localhost:8080')); + final service = EchoServiceClient(channel); + final app = EchoApp(service); + + final button = querySelector('#send') as ButtonElement; + button.onClick.listen((e) async { + final msg = querySelector('#msg') as TextInputElement; + final value = msg.value.trim(); + msg.value = ''; + + if (value.isEmpty) return false; + + if (value.indexOf(' ') > 0) { + final countStr = value.substring(0, value.indexOf(' ')); + final count = int.tryParse(countStr); + + if (count != null) { + app.repeatEcho(value.substring(value.indexOf(' ') + 1), count); + } else { + app.echo(value); + } + } else { + app.echo(value); + } + }); +}
diff --git a/grpc/example/helloworld/README.md b/grpc/example/helloworld/README.md new file mode 100644 index 0000000..817c60b --- /dev/null +++ b/grpc/example/helloworld/README.md
@@ -0,0 +1,66 @@ +# Description +The hello world server and client demonstrate how to use Dart gRPC libraries to +perform unary RPCs. + +See the definition of the hello world service in `protos/helloworld.proto`. + +# Run the sample code +To compile and run the example, assuming you are in the root of the helloworld +folder, i.e., .../example/helloworld/, first get the dependencies by running: + +```sh +$ pub get +``` +## Run TCP sample code + +Start the server: + +```sh +$ dart bin/server.dart +``` + +Likewise, to run the client: + +```sh +$ dart bin/client.dart +``` +## Run UDS sample code + +Start the server: + +```sh +$ dart bin/unix_server.dart +``` + +Likewise, to run the client: + +```sh +$ dart bin/unix_client.dart +``` + +>**Note** the `UDS` only support *nix plantform. + +# Regenerate the stubs + +If you have made changes to the message or service definition in +`protos/helloworld.proto` and need to regenerate the corresponding Dart files, +you will need to have protoc version 3.0.0 or higher and the Dart protoc plugin +version 0.7.9 or higher on your PATH. + +To install protoc, see the instructions on +[the Protocol Buffers website](https://developers.google.com/protocol-buffers/). + +The easiest way to get the Dart protoc plugin is by running + +```sh +$ pub global activate protoc_plugin +``` + +and follow the directions to add `~/.pub-cache/bin` to your PATH, if you haven't +already done so. + +You can now regenerate the Dart files by running + +```sh +$ protoc --dart_out=grpc:lib/src/generated -Iprotos protos/helloworld.proto +```
diff --git a/grpc/example/helloworld/bin/client.dart b/grpc/example/helloworld/bin/client.dart new file mode 100644 index 0000000..9c023a4 --- /dev/null +++ b/grpc/example/helloworld/bin/client.dart
@@ -0,0 +1,45 @@ +// Copyright (c) 2018, the gRPC project authors. Please see the AUTHORS file +// for details. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +/// Dart implementation of the gRPC helloworld.Greeter client. +import 'package:grpc/grpc.dart'; +import 'package:helloworld/src/generated/helloworld.pb.dart'; +import 'package:helloworld/src/generated/helloworld.pbgrpc.dart'; + +Future<void> main(List<String> args) async { + final channel = ClientChannel( + 'localhost', + port: 50051, + options: ChannelOptions( + credentials: ChannelCredentials.insecure(), + codecRegistry: + CodecRegistry(codecs: const [GzipCodec(), IdentityCodec()]), + ), + ); + final stub = GreeterClient(channel); + + final name = args.isNotEmpty ? args[0] : 'world'; + + try { + final response = await stub.sayHello( + HelloRequest()..name = name, + options: CallOptions(compression: const GzipCodec()), + ); + print('Greeter client received: ${response.message}'); + } catch (e) { + print('Caught error: $e'); + } + await channel.shutdown(); +}
diff --git a/grpc/example/helloworld/bin/server.dart b/grpc/example/helloworld/bin/server.dart new file mode 100644 index 0000000..bc8afea --- /dev/null +++ b/grpc/example/helloworld/bin/server.dart
@@ -0,0 +1,36 @@ +// Copyright (c) 2018, the gRPC project authors. Please see the AUTHORS file +// for details. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +/// Dart implementation of the gRPC helloworld.Greeter server. +import 'package:grpc/grpc.dart'; +import 'package:helloworld/src/generated/helloworld.pb.dart'; +import 'package:helloworld/src/generated/helloworld.pbgrpc.dart'; + +class GreeterService extends GreeterServiceBase { + @override + Future<HelloReply> sayHello(ServiceCall call, HelloRequest request) async { + return HelloReply()..message = 'Hello, ${request.name}!'; + } +} + +Future<void> main(List<String> args) async { + final server = Server( + [GreeterService()], + const <Interceptor>[], + CodecRegistry(codecs: const [GzipCodec(), IdentityCodec()]), + ); + await server.serve(port: 50051); + print('Server listening on port ${server.port}...'); +}
diff --git a/grpc/example/helloworld/bin/unix_client.dart b/grpc/example/helloworld/bin/unix_client.dart new file mode 100644 index 0000000..9d9ce54 --- /dev/null +++ b/grpc/example/helloworld/bin/unix_client.dart
@@ -0,0 +1,42 @@ +// Copyright (c) 2020, the gRPC project authors. Please see the AUTHORS file +// for details. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +/// Dart implementation of the gRPC helloworld.Greeter client. +import 'dart:io'; +import 'package:grpc/grpc.dart'; + +import 'package:helloworld/src/generated/helloworld.pb.dart'; +import 'package:helloworld/src/generated/helloworld.pbgrpc.dart'; + +Future<void> main(List<String> args) async { + final udsAddress = + InternetAddress('localhost', type: InternetAddressType.unix); + final channel = ClientChannel( + udsAddress, + port: 0, + options: const ChannelOptions(credentials: ChannelCredentials.insecure()), + ); + final stub = GreeterClient(channel); + + final name = args.isNotEmpty ? args[0] : 'world'; + + try { + final response = await stub.sayHello(HelloRequest()..name = name); + print('Greeter client received: ${response.message}'); + } catch (e) { + print('Caught error: $e'); + } + await channel.shutdown(); +}
diff --git a/grpc/example/helloworld/bin/unix_server.dart b/grpc/example/helloworld/bin/unix_server.dart new file mode 100644 index 0000000..d40d578 --- /dev/null +++ b/grpc/example/helloworld/bin/unix_server.dart
@@ -0,0 +1,36 @@ +// Copyright (c) 2020, the gRPC project authors. Please see the AUTHORS file +// for details. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +/// Dart implementation of the gRPC helloworld.Greeter server. +import 'dart:io'; +import 'package:grpc/grpc.dart'; + +import 'package:helloworld/src/generated/helloworld.pb.dart'; +import 'package:helloworld/src/generated/helloworld.pbgrpc.dart'; + +class GreeterService extends GreeterServiceBase { + @override + Future<HelloReply> sayHello(ServiceCall call, HelloRequest request) async { + return HelloReply()..message = 'Hello, ${request.name}!'; + } +} + +Future<void> main(List<String> args) async { + final udsAddress = + InternetAddress('localhost', type: InternetAddressType.unix); + final server = Server([GreeterService()]); + await server.serve(address: udsAddress); + print('Start UNIX Server @localhost...'); +}
diff --git a/grpc/example/helloworld/lib/src/generated/helloworld.pb.dart b/grpc/example/helloworld/lib/src/generated/helloworld.pb.dart new file mode 100644 index 0000000..8e2b370 --- /dev/null +++ b/grpc/example/helloworld/lib/src/generated/helloworld.pb.dart
@@ -0,0 +1,143 @@ +/// +// Generated code. Do not modify. +// source: helloworld.proto +// +// @dart = 2.7 +// ignore_for_file: annotate_overrides,camel_case_types,unnecessary_const,non_constant_identifier_names,library_prefixes,unused_import,unused_shown_name,return_of_invalid_type,unnecessary_this,prefer_final_fields + +import 'dart:core' as $core; + +import 'package:protobuf/protobuf.dart' as $pb; + +class HelloRequest extends $pb.GeneratedMessage { + static final $pb.BuilderInfo _i = $pb.BuilderInfo( + const $core.bool.fromEnvironment('protobuf.omit_message_names') + ? '' + : 'HelloRequest', + package: const $pb.PackageName( + const $core.bool.fromEnvironment('protobuf.omit_message_names') + ? '' + : 'helloworld'), + createEmptyInstance: create) + ..aOS( + 1, + const $core.bool.fromEnvironment('protobuf.omit_field_names') + ? '' + : 'name') + ..hasRequiredFields = false; + + HelloRequest._() : super(); + factory HelloRequest({ + $core.String name, + }) { + final _result = create(); + if (name != null) { + _result.name = name; + } + return _result; + } + factory HelloRequest.fromBuffer($core.List<$core.int> i, + [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromBuffer(i, r); + factory HelloRequest.fromJson($core.String i, + [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromJson(i, r); + @$core.Deprecated('Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.deepCopy] instead. ' + 'Will be removed in next major version') + HelloRequest clone() => HelloRequest()..mergeFromMessage(this); + @$core.Deprecated('Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.rebuild] instead. ' + 'Will be removed in next major version') + HelloRequest copyWith(void Function(HelloRequest) updates) => + super.copyWith((message) => + updates(message as HelloRequest)); // ignore: deprecated_member_use + $pb.BuilderInfo get info_ => _i; + @$core.pragma('dart2js:noInline') + static HelloRequest create() => HelloRequest._(); + HelloRequest createEmptyInstance() => create(); + static $pb.PbList<HelloRequest> createRepeated() => + $pb.PbList<HelloRequest>(); + @$core.pragma('dart2js:noInline') + static HelloRequest getDefault() => _defaultInstance ??= + $pb.GeneratedMessage.$_defaultFor<HelloRequest>(create); + static HelloRequest _defaultInstance; + + @$pb.TagNumber(1) + $core.String get name => $_getSZ(0); + @$pb.TagNumber(1) + set name($core.String v) { + $_setString(0, v); + } + + @$pb.TagNumber(1) + $core.bool hasName() => $_has(0); + @$pb.TagNumber(1) + void clearName() => clearField(1); +} + +class HelloReply extends $pb.GeneratedMessage { + static final $pb.BuilderInfo _i = $pb.BuilderInfo( + const $core.bool.fromEnvironment('protobuf.omit_message_names') + ? '' + : 'HelloReply', + package: const $pb.PackageName( + const $core.bool.fromEnvironment('protobuf.omit_message_names') + ? '' + : 'helloworld'), + createEmptyInstance: create) + ..aOS( + 1, + const $core.bool.fromEnvironment('protobuf.omit_field_names') + ? '' + : 'message') + ..hasRequiredFields = false; + + HelloReply._() : super(); + factory HelloReply({ + $core.String message, + }) { + final _result = create(); + if (message != null) { + _result.message = message; + } + return _result; + } + factory HelloReply.fromBuffer($core.List<$core.int> i, + [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromBuffer(i, r); + factory HelloReply.fromJson($core.String i, + [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromJson(i, r); + @$core.Deprecated('Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.deepCopy] instead. ' + 'Will be removed in next major version') + HelloReply clone() => HelloReply()..mergeFromMessage(this); + @$core.Deprecated('Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.rebuild] instead. ' + 'Will be removed in next major version') + HelloReply copyWith(void Function(HelloReply) updates) => + super.copyWith((message) => + updates(message as HelloReply)); // ignore: deprecated_member_use + $pb.BuilderInfo get info_ => _i; + @$core.pragma('dart2js:noInline') + static HelloReply create() => HelloReply._(); + HelloReply createEmptyInstance() => create(); + static $pb.PbList<HelloReply> createRepeated() => $pb.PbList<HelloReply>(); + @$core.pragma('dart2js:noInline') + static HelloReply getDefault() => _defaultInstance ??= + $pb.GeneratedMessage.$_defaultFor<HelloReply>(create); + static HelloReply _defaultInstance; + + @$pb.TagNumber(1) + $core.String get message => $_getSZ(0); + @$pb.TagNumber(1) + set message($core.String v) { + $_setString(0, v); + } + + @$pb.TagNumber(1) + $core.bool hasMessage() => $_has(0); + @$pb.TagNumber(1) + void clearMessage() => clearField(1); +}
diff --git a/grpc/example/helloworld/lib/src/generated/helloworld.pbenum.dart b/grpc/example/helloworld/lib/src/generated/helloworld.pbenum.dart new file mode 100644 index 0000000..8465a3e --- /dev/null +++ b/grpc/example/helloworld/lib/src/generated/helloworld.pbenum.dart
@@ -0,0 +1,6 @@ +/// +// Generated code. Do not modify. +// source: helloworld.proto +// +// @dart = 2.7 +// ignore_for_file: annotate_overrides,camel_case_types,unnecessary_const,non_constant_identifier_names,library_prefixes,unused_import,unused_shown_name,return_of_invalid_type,unnecessary_this,prefer_final_fields
diff --git a/grpc/example/helloworld/lib/src/generated/helloworld.pbgrpc.dart b/grpc/example/helloworld/lib/src/generated/helloworld.pbgrpc.dart new file mode 100644 index 0000000..81fb11e --- /dev/null +++ b/grpc/example/helloworld/lib/src/generated/helloworld.pbgrpc.dart
@@ -0,0 +1,53 @@ +/// +// Generated code. Do not modify. +// source: helloworld.proto +// +// @dart = 2.7 +// ignore_for_file: annotate_overrides,camel_case_types,unnecessary_const,non_constant_identifier_names,library_prefixes,unused_import,unused_shown_name,return_of_invalid_type,unnecessary_this,prefer_final_fields + +import 'dart:async' as $async; + +import 'dart:core' as $core; + +import 'package:grpc/service_api.dart' as $grpc; +import 'helloworld.pb.dart' as $0; +export 'helloworld.pb.dart'; + +class GreeterClient extends $grpc.Client { + static final _$sayHello = $grpc.ClientMethod<$0.HelloRequest, $0.HelloReply>( + '/helloworld.Greeter/SayHello', + ($0.HelloRequest value) => value.writeToBuffer(), + ($core.List<$core.int> value) => $0.HelloReply.fromBuffer(value)); + + GreeterClient($grpc.ClientChannel channel, + {$grpc.CallOptions options, + $core.Iterable<$grpc.ClientInterceptor> interceptors}) + : super(channel, options: options, interceptors: interceptors); + + $grpc.ResponseFuture<$0.HelloReply> sayHello($0.HelloRequest request, + {$grpc.CallOptions options}) { + return $createUnaryCall(_$sayHello, request, options: options); + } +} + +abstract class GreeterServiceBase extends $grpc.Service { + $core.String get $name => 'helloworld.Greeter'; + + GreeterServiceBase() { + $addMethod($grpc.ServiceMethod<$0.HelloRequest, $0.HelloReply>( + 'SayHello', + sayHello_Pre, + false, + false, + ($core.List<$core.int> value) => $0.HelloRequest.fromBuffer(value), + ($0.HelloReply value) => value.writeToBuffer())); + } + + $async.Future<$0.HelloReply> sayHello_Pre( + $grpc.ServiceCall call, $async.Future<$0.HelloRequest> request) async { + return sayHello(call, await request); + } + + $async.Future<$0.HelloReply> sayHello( + $grpc.ServiceCall call, $0.HelloRequest request); +}
diff --git a/grpc/example/helloworld/lib/src/generated/helloworld.pbjson.dart b/grpc/example/helloworld/lib/src/generated/helloworld.pbjson.dart new file mode 100644 index 0000000..3aced27 --- /dev/null +++ b/grpc/example/helloworld/lib/src/generated/helloworld.pbjson.dart
@@ -0,0 +1,20 @@ +/// +// Generated code. Do not modify. +// source: helloworld.proto +// +// @dart = 2.7 +// ignore_for_file: annotate_overrides,camel_case_types,unnecessary_const,non_constant_identifier_names,library_prefixes,unused_import,unused_shown_name,return_of_invalid_type,unnecessary_this,prefer_final_fields + +const HelloRequest$json = const { + '1': 'HelloRequest', + '2': const [ + const {'1': 'name', '3': 1, '4': 1, '5': 9, '10': 'name'}, + ], +}; + +const HelloReply$json = const { + '1': 'HelloReply', + '2': const [ + const {'1': 'message', '3': 1, '4': 1, '5': 9, '10': 'message'}, + ], +};
diff --git a/grpc/example/helloworld/protos/helloworld.proto b/grpc/example/helloworld/protos/helloworld.proto new file mode 100644 index 0000000..be878ce --- /dev/null +++ b/grpc/example/helloworld/protos/helloworld.proto
@@ -0,0 +1,38 @@ +// Copyright 2015 gRPC authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +syntax = "proto3"; + +option java_multiple_files = true; +option java_package = "io.grpc.examples.helloworld"; +option java_outer_classname = "HelloWorldProto"; +option objc_class_prefix = "HLW"; + +package helloworld; + +// The greeting service definition. +service Greeter { + // Sends a greeting + rpc SayHello (HelloRequest) returns (HelloReply) {} +} + +// The request message containing the user's name. +message HelloRequest { + string name = 1; +} + +// The response message containing the greetings +message HelloReply { + string message = 1; +}
diff --git a/grpc/example/helloworld/pubspec.yaml b/grpc/example/helloworld/pubspec.yaml new file mode 100644 index 0000000..3df0719 --- /dev/null +++ b/grpc/example/helloworld/pubspec.yaml
@@ -0,0 +1,12 @@ +name: helloworld +description: Dart gRPC sample client and server. +publish_to: none + +environment: + sdk: '>=2.11.99 <3.0.0' + +dependencies: + async: ^2.2.0 + grpc: + path: ../../ + protobuf: ^2.0.0
diff --git a/grpc/example/metadata/README.md b/grpc/example/metadata/README.md new file mode 100644 index 0000000..01f1e4a --- /dev/null +++ b/grpc/example/metadata/README.md
@@ -0,0 +1,53 @@ +# Description +The metadata server and client demonstrate how to handle custom metadata, +cancellation, and timeouts in Dart gRPC. + +See the definition of the metadata service in `protos/metadata.proto`. + +# Run the sample code +To compile and run the example, assuming you are in the root of the metadata +folder, i.e., .../example/metadata/, first get the dependencies by running: + +```sh +$ pub get +``` + +Then, to run the server: + +```sh +$ dart bin/server.dart +``` + +Likewise, to run the client: + +```sh +$ dart bin/client.dart +``` + +# Regenerate the stubs + +If you have made changes to the message or service definition in +`protos/route_guide.proto` and need to regenerate the corresponding Dart files, +you will need to have protoc version 3.0.0 or higher and the Dart protoc plugin +version 0.7.9 or higher on your PATH. + +To install protoc with Dart support, take these steps: + +1. Install the `protoc` matching your development operating system from +[the Protocol Buffers releases page](https://github.com/google/protobuf/releases) +(e.g. `protoc-3.5.1-osx-x86_64.zip` for macOS). + +1. Get the Dart protoc plugin by running + + ```sh + $ pub global activate protoc_plugin + ``` + +1. Add `~/.pub-cache/bin` to your PATH, if you haven't +already done so. + +You can now regenerate the Dart files by running + +```sh +$ protoc --dart_out=grpc:lib/src/generated -Iprotos protos/metadata.proto +```
diff --git a/grpc/example/metadata/bin/client.dart b/grpc/example/metadata/bin/client.dart new file mode 100644 index 0000000..66e3479 --- /dev/null +++ b/grpc/example/metadata/bin/client.dart
@@ -0,0 +1,20 @@ +// Copyright (c) 2017, the gRPC project authors. Please see the AUTHORS file +// for details. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import 'package:metadata/src/client.dart'; + +void main(List<String> args) { + Client().main(args); +}
diff --git a/grpc/example/metadata/bin/server.dart b/grpc/example/metadata/bin/server.dart new file mode 100644 index 0000000..6b1819a --- /dev/null +++ b/grpc/example/metadata/bin/server.dart
@@ -0,0 +1,20 @@ +// Copyright (c) 2017, the gRPC project authors. Please see the AUTHORS file +// for details. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import 'package:metadata/src/server.dart'; + +void main(List<String> args) { + Server().main(args); +}
diff --git a/grpc/example/metadata/lib/src/client.dart b/grpc/example/metadata/lib/src/client.dart new file mode 100644 index 0000000..6644304 --- /dev/null +++ b/grpc/example/metadata/lib/src/client.dart
@@ -0,0 +1,149 @@ +// Copyright (c) 2017, the gRPC project authors. Please see the AUTHORS file +// for details. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import 'dart:async'; + +import 'package:grpc/grpc.dart'; + +import 'generated/metadata.pbgrpc.dart'; + +class Client { + ClientChannel channel; + MetadataClient stub; + + Future<void> main(List<String> args) async { + channel = ClientChannel('127.0.0.1', + port: 8080, + options: + const ChannelOptions(credentials: ChannelCredentials.insecure())); + stub = MetadataClient(channel); + // Run all of the demos in order. + await runEcho(); + await runEchoDelayCancel(); + await runAddOneCancel(); + await runFibonacciCancel(); + await runFibonacciTimeout(); + await channel.shutdown(); + } + + /// Run the echo demo. + /// + /// Send custom metadata with a RPC, and print out the received response and + /// metadata. + Future<void> runEcho() async { + final request = Record()..value = 'Kaj'; + final call = + stub.echo(request, options: CallOptions(metadata: {'peer': 'Verner'})); + call.headers.then((headers) { + print('Received header metadata: $headers'); + }); + call.trailers.then((trailers) { + print('Received trailer metadata: $trailers'); + }); + final response = await call; + print('Echo response: ${response.value}'); + } + + /// Run the echo with delay cancel demo. + /// + /// Same as the echo demo, but demonstrating per-client custom metadata, as + /// well as a per-call metadata. The server will delay the response for the + /// requested duration, during which the client will cancel the RPC. + Future<void> runEchoDelayCancel() async { + final stubWithCustomOptions = MetadataClient(channel, + options: CallOptions(metadata: {'peer': 'Verner'})); + final request = Record()..value = 'Kaj'; + final call = stubWithCustomOptions.echo(request, + options: CallOptions(metadata: {'delay': '1'})); + call.headers.then((headers) { + print('Received header metadata: $headers'); + }); + call.trailers.then((trailers) { + print('Received trailer metadata: $trailers'); + }); + await Future.delayed(Duration(milliseconds: 10)); + call.cancel(); + try { + final response = await call; + print('Unexpected echo response: ${response.value}'); + } catch (error) { + print('Expected error: $error'); + } + } + + /// Run the addOne cancel demo. + /// + /// Makes a bi-directional RPC, sends 4 requests, and cancels the RPC after + /// receiving 3 responses. + Future<void> runAddOneCancel() async { + final numbers = StreamController<int>(); + final call = + stub.addOne(numbers.stream.map((value) => Number()..value = value)); + final receivedThree = Completer<bool>(); + final sub = call.listen((number) { + print('AddOneCancel: Received ${number.value}'); + if (number.value == 3) { + receivedThree.complete(true); + } + }, onError: (e) => print('Caught: $e')); + numbers.add(1); + numbers.add(2); + numbers.add(3); + numbers.add(4); + await receivedThree.future; + await call.cancel(); + await Future.wait([sub.cancel(), numbers.close()]); + } + + /// Run the Fibonacci demo. + /// + /// Call an RPC that returns a stream of Fibonacci numbers. Cancel the call + /// after receiving more than 5 responses. + Future<void> runFibonacciCancel() async { + final call = stub.fibonacci(Empty()); + var count = 0; + try { + await for (var number in call) { + count++; + print('Received ${number.value} (count=$count)'); + if (count > 5) { + await call.cancel(); + } + } + } on GrpcError catch (e) { + print('Caught: $e'); + } + print('Final count: $count'); + } + + /// Run the timeout demo. + /// + /// Call an RPC that returns a stream of Fibonacci numbers, and specify an RPC + /// timeout of 2 seconds. + Future<void> runFibonacciTimeout() async { + final call = stub.fibonacci(Empty(), + options: CallOptions(timeout: Duration(seconds: 2))); + var count = 0; + try { + await for (var number in call) { + count++; + print('Received ${number.value} (count=$count)'); + } + } on GrpcError catch (e) { + print('Caught: $e'); + } + print('Final count: $count'); + } +}
diff --git a/grpc/example/metadata/lib/src/generated/metadata.pb.dart b/grpc/example/metadata/lib/src/generated/metadata.pb.dart new file mode 100644 index 0000000..daffff1 --- /dev/null +++ b/grpc/example/metadata/lib/src/generated/metadata.pb.dart
@@ -0,0 +1,181 @@ +/// +// Generated code. Do not modify. +// source: metadata.proto +// +// @dart = 2.7 +// ignore_for_file: annotate_overrides,camel_case_types,unnecessary_const,non_constant_identifier_names,library_prefixes,unused_import,unused_shown_name,return_of_invalid_type,unnecessary_this,prefer_final_fields + +import 'dart:core' as $core; + +import 'package:protobuf/protobuf.dart' as $pb; + +class Record extends $pb.GeneratedMessage { + static final $pb.BuilderInfo _i = $pb.BuilderInfo( + const $core.bool.fromEnvironment('protobuf.omit_message_names') + ? '' + : 'Record', + package: const $pb.PackageName( + const $core.bool.fromEnvironment('protobuf.omit_message_names') + ? '' + : 'grpc'), + createEmptyInstance: create) + ..aOS( + 1, + const $core.bool.fromEnvironment('protobuf.omit_field_names') + ? '' + : 'value') + ..hasRequiredFields = false; + + Record._() : super(); + factory Record({ + $core.String value, + }) { + final _result = create(); + if (value != null) { + _result.value = value; + } + return _result; + } + factory Record.fromBuffer($core.List<$core.int> i, + [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromBuffer(i, r); + factory Record.fromJson($core.String i, + [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromJson(i, r); + @$core.Deprecated('Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.deepCopy] instead. ' + 'Will be removed in next major version') + Record clone() => Record()..mergeFromMessage(this); + @$core.Deprecated('Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.rebuild] instead. ' + 'Will be removed in next major version') + Record copyWith(void Function(Record) updates) => super.copyWith( + (message) => updates(message as Record)); // ignore: deprecated_member_use + $pb.BuilderInfo get info_ => _i; + @$core.pragma('dart2js:noInline') + static Record create() => Record._(); + Record createEmptyInstance() => create(); + static $pb.PbList<Record> createRepeated() => $pb.PbList<Record>(); + @$core.pragma('dart2js:noInline') + static Record getDefault() => + _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor<Record>(create); + static Record _defaultInstance; + + @$pb.TagNumber(1) + $core.String get value => $_getSZ(0); + @$pb.TagNumber(1) + set value($core.String v) { + $_setString(0, v); + } + + @$pb.TagNumber(1) + $core.bool hasValue() => $_has(0); + @$pb.TagNumber(1) + void clearValue() => clearField(1); +} + +class Number extends $pb.GeneratedMessage { + static final $pb.BuilderInfo _i = $pb.BuilderInfo( + const $core.bool.fromEnvironment('protobuf.omit_message_names') + ? '' + : 'Number', + package: const $pb.PackageName( + const $core.bool.fromEnvironment('protobuf.omit_message_names') + ? '' + : 'grpc'), + createEmptyInstance: create) + ..a<$core.int>( + 1, + const $core.bool.fromEnvironment('protobuf.omit_field_names') + ? '' + : 'value', + $pb.PbFieldType.O3) + ..hasRequiredFields = false; + + Number._() : super(); + factory Number({ + $core.int value, + }) { + final _result = create(); + if (value != null) { + _result.value = value; + } + return _result; + } + factory Number.fromBuffer($core.List<$core.int> i, + [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromBuffer(i, r); + factory Number.fromJson($core.String i, + [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromJson(i, r); + @$core.Deprecated('Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.deepCopy] instead. ' + 'Will be removed in next major version') + Number clone() => Number()..mergeFromMessage(this); + @$core.Deprecated('Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.rebuild] instead. ' + 'Will be removed in next major version') + Number copyWith(void Function(Number) updates) => super.copyWith( + (message) => updates(message as Number)); // ignore: deprecated_member_use + $pb.BuilderInfo get info_ => _i; + @$core.pragma('dart2js:noInline') + static Number create() => Number._(); + Number createEmptyInstance() => create(); + static $pb.PbList<Number> createRepeated() => $pb.PbList<Number>(); + @$core.pragma('dart2js:noInline') + static Number getDefault() => + _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor<Number>(create); + static Number _defaultInstance; + + @$pb.TagNumber(1) + $core.int get value => $_getIZ(0); + @$pb.TagNumber(1) + set value($core.int v) { + $_setSignedInt32(0, v); + } + + @$pb.TagNumber(1) + $core.bool hasValue() => $_has(0); + @$pb.TagNumber(1) + void clearValue() => clearField(1); +} + +class Empty extends $pb.GeneratedMessage { + static final $pb.BuilderInfo _i = $pb.BuilderInfo( + const $core.bool.fromEnvironment('protobuf.omit_message_names') + ? '' + : 'Empty', + package: const $pb.PackageName( + const $core.bool.fromEnvironment('protobuf.omit_message_names') + ? '' + : 'grpc'), + createEmptyInstance: create) + ..hasRequiredFields = false; + + Empty._() : super(); + factory Empty() => create(); + factory Empty.fromBuffer($core.List<$core.int> i, + [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromBuffer(i, r); + factory Empty.fromJson($core.String i, + [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromJson(i, r); + @$core.Deprecated('Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.deepCopy] instead. ' + 'Will be removed in next major version') + Empty clone() => Empty()..mergeFromMessage(this); + @$core.Deprecated('Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.rebuild] instead. ' + 'Will be removed in next major version') + Empty copyWith(void Function(Empty) updates) => super.copyWith( + (message) => updates(message as Empty)); // ignore: deprecated_member_use + $pb.BuilderInfo get info_ => _i; + @$core.pragma('dart2js:noInline') + static Empty create() => Empty._(); + Empty createEmptyInstance() => create(); + static $pb.PbList<Empty> createRepeated() => $pb.PbList<Empty>(); + @$core.pragma('dart2js:noInline') + static Empty getDefault() => + _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor<Empty>(create); + static Empty _defaultInstance; +}
diff --git a/grpc/example/metadata/lib/src/generated/metadata.pbgrpc.dart b/grpc/example/metadata/lib/src/generated/metadata.pbgrpc.dart new file mode 100644 index 0000000..cf13cc0 --- /dev/null +++ b/grpc/example/metadata/lib/src/generated/metadata.pbgrpc.dart
@@ -0,0 +1,94 @@ +/// +// Generated code. Do not modify. +// source: metadata.proto +// +// @dart = 2.7 +// ignore_for_file: annotate_overrides,camel_case_types,unnecessary_const,non_constant_identifier_names,library_prefixes,unused_import,unused_shown_name,return_of_invalid_type,unnecessary_this,prefer_final_fields + +import 'dart:async' as $async; + +import 'dart:core' as $core; + +import 'package:grpc/service_api.dart' as $grpc; +import 'metadata.pb.dart' as $0; +export 'metadata.pb.dart'; + +class MetadataClient extends $grpc.Client { + static final _$echo = $grpc.ClientMethod<$0.Record, $0.Record>( + '/grpc.Metadata/Echo', + ($0.Record value) => value.writeToBuffer(), + ($core.List<$core.int> value) => $0.Record.fromBuffer(value)); + static final _$addOne = $grpc.ClientMethod<$0.Number, $0.Number>( + '/grpc.Metadata/AddOne', + ($0.Number value) => value.writeToBuffer(), + ($core.List<$core.int> value) => $0.Number.fromBuffer(value)); + static final _$fibonacci = $grpc.ClientMethod<$0.Empty, $0.Number>( + '/grpc.Metadata/Fibonacci', + ($0.Empty value) => value.writeToBuffer(), + ($core.List<$core.int> value) => $0.Number.fromBuffer(value)); + + MetadataClient($grpc.ClientChannel channel, + {$grpc.CallOptions options, + $core.Iterable<$grpc.ClientInterceptor> interceptors}) + : super(channel, options: options, interceptors: interceptors); + + $grpc.ResponseFuture<$0.Record> echo($0.Record request, + {$grpc.CallOptions options}) { + return $createUnaryCall(_$echo, request, options: options); + } + + $grpc.ResponseStream<$0.Number> addOne($async.Stream<$0.Number> request, + {$grpc.CallOptions options}) { + return $createStreamingCall(_$addOne, request, options: options); + } + + $grpc.ResponseStream<$0.Number> fibonacci($0.Empty request, + {$grpc.CallOptions options}) { + return $createStreamingCall( + _$fibonacci, $async.Stream.fromIterable([request]), + options: options); + } +} + +abstract class MetadataServiceBase extends $grpc.Service { + $core.String get $name => 'grpc.Metadata'; + + MetadataServiceBase() { + $addMethod($grpc.ServiceMethod<$0.Record, $0.Record>( + 'Echo', + echo_Pre, + false, + false, + ($core.List<$core.int> value) => $0.Record.fromBuffer(value), + ($0.Record value) => value.writeToBuffer())); + $addMethod($grpc.ServiceMethod<$0.Number, $0.Number>( + 'AddOne', + addOne, + true, + true, + ($core.List<$core.int> value) => $0.Number.fromBuffer(value), + ($0.Number value) => value.writeToBuffer())); + $addMethod($grpc.ServiceMethod<$0.Empty, $0.Number>( + 'Fibonacci', + fibonacci_Pre, + false, + true, + ($core.List<$core.int> value) => $0.Empty.fromBuffer(value), + ($0.Number value) => value.writeToBuffer())); + } + + $async.Future<$0.Record> echo_Pre( + $grpc.ServiceCall call, $async.Future<$0.Record> request) async { + return echo(call, await request); + } + + $async.Stream<$0.Number> fibonacci_Pre( + $grpc.ServiceCall call, $async.Future<$0.Empty> request) async* { + yield* fibonacci(call, await request); + } + + $async.Future<$0.Record> echo($grpc.ServiceCall call, $0.Record request); + $async.Stream<$0.Number> addOne( + $grpc.ServiceCall call, $async.Stream<$0.Number> request); + $async.Stream<$0.Number> fibonacci($grpc.ServiceCall call, $0.Empty request); +}
diff --git a/grpc/example/metadata/lib/src/server.dart b/grpc/example/metadata/lib/src/server.dart new file mode 100644 index 0000000..1f49424 --- /dev/null +++ b/grpc/example/metadata/lib/src/server.dart
@@ -0,0 +1,85 @@ +// Copyright (c) 2017, the gRPC project authors. Please see the AUTHORS file +// for details. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import 'dart:async'; + +import 'package:grpc/grpc.dart' as grpc; + +import 'generated/metadata.pbgrpc.dart'; + +class MetadataService extends MetadataServiceBase { + int callCount = 0; + + @override + Future<Record> echo(grpc.ServiceCall call, Record request) async { + final peer = call.clientMetadata['peer']; + final count = callCount++; + print('Echo: Call #$count: Peer: $peer, request: ${request.value}'); + call.headers['count'] = '${count}'; + call.trailers['hello'] = request.value; + + final delay = call.clientMetadata['delay']; + if (delay != null) { + await Future.delayed(Duration(seconds: int.parse(delay))); + } + + return Record()..value = peer; + } + + @override + Stream<Number> addOne(grpc.ServiceCall call, Stream<Number> request) async* { + var lastNumber = -1; + try { + await for (var number in request) { + lastNumber = number.value; + yield Number()..value = number.value + 1; + } + } catch (error) { + print('Caught: $error, last number = $lastNumber'); + } finally { + if (call.isCanceled) { + print('AddOne: Call canceled'); + } + } + } + + /// Streams a Fibonacci number every 500ms until the call is canceled. + @override + Stream<Number> fibonacci(grpc.ServiceCall call, Empty request) async* { + var previous = 0; + var current = 1; + try { + while (true) { + await Future.delayed(Duration(milliseconds: 500)); + yield Number()..value = current; + final next = current + previous; + previous = current; + current = next; + } + } finally { + if (call.isCanceled) { + print('Fibonacci: Canceled.'); + } + } + } +} + +class Server { + Future<void> main(List<String> args) async { + final server = grpc.Server([MetadataService()]); + await server.serve(port: 8080); + print('Server listening on port ${server.port}...'); + } +}
diff --git a/grpc/example/metadata/protos/metadata.proto b/grpc/example/metadata/protos/metadata.proto new file mode 100644 index 0000000..9b6a63f --- /dev/null +++ b/grpc/example/metadata/protos/metadata.proto
@@ -0,0 +1,67 @@ +// Copyright 2017, Google Inc. +// All rights reserved. +// +// 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 Inc. 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. + +syntax = "proto3"; + +package grpc; + +// Interface exported by the server. +service Metadata { + // Echo metadata. + // + // Echoes the given input as trailer metadata. Sets a call counter as header + // metadata, and returns the value of the 'hello' key in the client metadata + // as the result. + rpc Echo(Record) returns (Record) {} + + // Adds 1 to the numbers in the request stream. + // + // Uses bidirectional streaming. + rpc AddOne(stream Number) returns (stream Number) {} + + // Fibonacci. + // + // Streams Fibonacci numbers until the call is canceled or times out. + rpc Fibonacci(Empty) returns (stream Number) {} +} + +// A message containing a single string value. +message Record { + string value = 1; +} + +// A message containing a single number. +message Number { + int32 value = 1; +} + +// A message containing nothing. +message Empty { +} +
diff --git a/grpc/example/metadata/pubspec.yaml b/grpc/example/metadata/pubspec.yaml new file mode 100644 index 0000000..6e84551 --- /dev/null +++ b/grpc/example/metadata/pubspec.yaml
@@ -0,0 +1,15 @@ +name: metadata +description: Dart gRPC sample client and server. +publish_to: none + +environment: + sdk: '>=2.11.99 <3.0.0' + +dependencies: + async: ^2.2.0 + grpc: + path: ../../ + protobuf: ^2.0.0 + +dev_dependencies: + test: ^1.6.0
diff --git a/grpc/example/metadata/tool/regenerate.sh b/grpc/example/metadata/tool/regenerate.sh new file mode 100755 index 0000000..65e2a27 --- /dev/null +++ b/grpc/example/metadata/tool/regenerate.sh
@@ -0,0 +1,4 @@ +#!/usr/bin/env bash +protoc --dart_out=grpc:lib/src/generated -Iprotos protos/metadata.proto +rm lib/src/generated/metadata.pb{enum,json}.dart +dartfmt -w lib/src/generated
diff --git a/grpc/example/route_guide/README.md b/grpc/example/route_guide/README.md new file mode 100644 index 0000000..ece2692 --- /dev/null +++ b/grpc/example/route_guide/README.md
@@ -0,0 +1,53 @@ +# Description +The route guide server and client demonstrate how to use Dart gRPC libraries to +perform unary, client streaming, server streaming and full duplex RPCs. + +See the definition of the route guide service in `protos/route_guide.proto`. + +# Run the sample code +To compile and run the example, assuming you are in the root of the route_guide +folder, i.e., .../example/route_guide/, first get the dependencies by running: + +```sh +$ pub get +``` + +Then, to run the server: + +```sh +$ dart bin/server.dart +``` + +Likewise, to run the client: + +```sh +$ dart bin/client.dart +``` + +# Regenerate the stubs + +If you have made changes to the message or service definition in +`protos/route_guide.proto` and need to regenerate the corresponding Dart files, +you will need to have protoc version 3.0.0 or higher and the Dart protoc plugin +version 0.7.9 or higher on your PATH. + +To install protoc with Dart support, take these steps: + +1. Install the `protoc` matching your development operating system from +[the Protocol Buffers releases page](https://github.com/google/protobuf/releases) +(e.g. `protoc-3.5.1-osx-x86_64.zip` for macOS). + +1. Get the Dart protoc plugin by running + + ```sh + $ pub global activate protoc_plugin + ``` + +1. Add `~/.pub-cache/bin` to your PATH, if you haven't +already done so. + +You can now regenerate the Dart files by running + +```sh +$ protoc --dart_out=grpc:lib/src/generated -Iprotos protos/route_guide.proto +```
diff --git a/grpc/example/route_guide/bin/client.dart b/grpc/example/route_guide/bin/client.dart new file mode 100644 index 0000000..f816550 --- /dev/null +++ b/grpc/example/route_guide/bin/client.dart
@@ -0,0 +1,20 @@ +// Copyright (c) 2017, the gRPC project authors. Please see the AUTHORS file +// for details. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import 'package:route_guide/src/client.dart'; + +Future<void> main(List<String> args) async { + await Client().main(args); +}
diff --git a/grpc/example/route_guide/bin/server.dart b/grpc/example/route_guide/bin/server.dart new file mode 100644 index 0000000..69bcba0 --- /dev/null +++ b/grpc/example/route_guide/bin/server.dart
@@ -0,0 +1,20 @@ +// Copyright (c) 2017, the gRPC project authors. Please see the AUTHORS file +// for details. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import 'package:route_guide/src/server.dart'; + +Future<void> main(List<String> args) async { + await Server().main(args); +}
diff --git a/grpc/example/route_guide/data/route_guide_db.json b/grpc/example/route_guide/data/route_guide_db.json new file mode 100644 index 0000000..9d6a980 --- /dev/null +++ b/grpc/example/route_guide/data/route_guide_db.json
@@ -0,0 +1,601 @@ +[{ + "location": { + "latitude": 407838351, + "longitude": -746143763 + }, + "name": "Patriots Path, Mendham, NJ 07945, USA" +}, { + "location": { + "latitude": 408122808, + "longitude": -743999179 + }, + "name": "101 New Jersey 10, Whippany, NJ 07981, USA" +}, { + "location": { + "latitude": 413628156, + "longitude": -749015468 + }, + "name": "U.S. 6, Shohola, PA 18458, USA" +}, { + "location": { + "latitude": 419999544, + "longitude": -740371136 + }, + "name": "5 Conners Road, Kingston, NY 12401, USA" +}, { + "location": { + "latitude": 414008389, + "longitude": -743951297 + }, + "name": "Mid Hudson Psychiatric Center, New Hampton, NY 10958, USA" +}, { + "location": { + "latitude": 419611318, + "longitude": -746524769 + }, + "name": "287 Flugertown Road, Livingston Manor, NY 12758, USA" +}, { + "location": { + "latitude": 406109563, + "longitude": -742186778 + }, + "name": "4001 Tremley Point Road, Linden, NJ 07036, USA" +}, { + "location": { + "latitude": 416802456, + "longitude": -742370183 + }, + "name": "352 South Mountain Road, Wallkill, NY 12589, USA" +}, { + "location": { + "latitude": 412950425, + "longitude": -741077389 + }, + "name": "Bailey Turn Road, Harriman, NY 10926, USA" +}, { + "location": { + "latitude": 412144655, + "longitude": -743949739 + }, + "name": "193-199 Wawayanda Road, Hewitt, NJ 07421, USA" +}, { + "location": { + "latitude": 415736605, + "longitude": -742847522 + }, + "name": "406-496 Ward Avenue, Pine Bush, NY 12566, USA" +}, { + "location": { + "latitude": 413843930, + "longitude": -740501726 + }, + "name": "162 Merrill Road, Highland Mills, NY 10930, USA" +}, { + "location": { + "latitude": 410873075, + "longitude": -744459023 + }, + "name": "Clinton Road, West Milford, NJ 07480, USA" +}, { + "location": { + "latitude": 412346009, + "longitude": -744026814 + }, + "name": "16 Old Brook Lane, Warwick, NY 10990, USA" +}, { + "location": { + "latitude": 402948455, + "longitude": -747903913 + }, + "name": "3 Drake Lane, Pennington, NJ 08534, USA" +}, { + "location": { + "latitude": 406337092, + "longitude": -740122226 + }, + "name": "6324 8th Avenue, Brooklyn, NY 11220, USA" +}, { + "location": { + "latitude": 406421967, + "longitude": -747727624 + }, + "name": "1 Merck Access Road, Whitehouse Station, NJ 08889, USA" +}, { + "location": { + "latitude": 416318082, + "longitude": -749677716 + }, + "name": "78-98 Schalck Road, Narrowsburg, NY 12764, USA" +}, { + "location": { + "latitude": 415301720, + "longitude": -748416257 + }, + "name": "282 Lakeview Drive Road, Highland Lake, NY 12743, USA" +}, { + "location": { + "latitude": 402647019, + "longitude": -747071791 + }, + "name": "330 Evelyn Avenue, Hamilton Township, NJ 08619, USA" +}, { + "location": { + "latitude": 412567807, + "longitude": -741058078 + }, + "name": "New York State Reference Route 987E, Southfields, NY 10975, USA" +}, { + "location": { + "latitude": 416855156, + "longitude": -744420597 + }, + "name": "103-271 Tempaloni Road, Ellenville, NY 12428, USA" +}, { + "location": { + "latitude": 404663628, + "longitude": -744820157 + }, + "name": "1300 Airport Road, North Brunswick Township, NJ 08902, USA" +}, { + "location": { + "latitude": 407113723, + "longitude": -749746483 + }, + "name": "" +}, { + "location": { + "latitude": 402133926, + "longitude": -743613249 + }, + "name": "" +}, { + "location": { + "latitude": 400273442, + "longitude": -741220915 + }, + "name": "" +}, { + "location": { + "latitude": 411236786, + "longitude": -744070769 + }, + "name": "" +}, { + "location": { + "latitude": 411633782, + "longitude": -746784970 + }, + "name": "211-225 Plains Road, Augusta, NJ 07822, USA" +}, { + "location": { + "latitude": 415830701, + "longitude": -742952812 + }, + "name": "" +}, { + "location": { + "latitude": 413447164, + "longitude": -748712898 + }, + "name": "165 Pedersen Ridge Road, Milford, PA 18337, USA" +}, { + "location": { + "latitude": 405047245, + "longitude": -749800722 + }, + "name": "100-122 Locktown Road, Frenchtown, NJ 08825, USA" +}, { + "location": { + "latitude": 418858923, + "longitude": -746156790 + }, + "name": "" +}, { + "location": { + "latitude": 417951888, + "longitude": -748484944 + }, + "name": "650-652 Willi Hill Road, Swan Lake, NY 12783, USA" +}, { + "location": { + "latitude": 407033786, + "longitude": -743977337 + }, + "name": "26 East 3rd Street, New Providence, NJ 07974, USA" +}, { + "location": { + "latitude": 417548014, + "longitude": -740075041 + }, + "name": "" +}, { + "location": { + "latitude": 410395868, + "longitude": -744972325 + }, + "name": "" +}, { + "location": { + "latitude": 404615353, + "longitude": -745129803 + }, + "name": "" +}, { + "location": { + "latitude": 406589790, + "longitude": -743560121 + }, + "name": "611 Lawrence Avenue, Westfield, NJ 07090, USA" +}, { + "location": { + "latitude": 414653148, + "longitude": -740477477 + }, + "name": "18 Lannis Avenue, New Windsor, NY 12553, USA" +}, { + "location": { + "latitude": 405957808, + "longitude": -743255336 + }, + "name": "82-104 Amherst Avenue, Colonia, NJ 07067, USA" +}, { + "location": { + "latitude": 411733589, + "longitude": -741648093 + }, + "name": "170 Seven Lakes Drive, Sloatsburg, NY 10974, USA" +}, { + "location": { + "latitude": 412676291, + "longitude": -742606606 + }, + "name": "1270 Lakes Road, Monroe, NY 10950, USA" +}, { + "location": { + "latitude": 409224445, + "longitude": -748286738 + }, + "name": "509-535 Alphano Road, Great Meadows, NJ 07838, USA" +}, { + "location": { + "latitude": 406523420, + "longitude": -742135517 + }, + "name": "652 Garden Street, Elizabeth, NJ 07202, USA" +}, { + "location": { + "latitude": 401827388, + "longitude": -740294537 + }, + "name": "349 Sea Spray Court, Neptune City, NJ 07753, USA" +}, { + "location": { + "latitude": 410564152, + "longitude": -743685054 + }, + "name": "13-17 Stanley Street, West Milford, NJ 07480, USA" +}, { + "location": { + "latitude": 408472324, + "longitude": -740726046 + }, + "name": "47 Industrial Avenue, Teterboro, NJ 07608, USA" +}, { + "location": { + "latitude": 412452168, + "longitude": -740214052 + }, + "name": "5 White Oak Lane, Stony Point, NY 10980, USA" +}, { + "location": { + "latitude": 409146138, + "longitude": -746188906 + }, + "name": "Berkshire Valley Management Area Trail, Jefferson, NJ, USA" +}, { + "location": { + "latitude": 404701380, + "longitude": -744781745 + }, + "name": "1007 Jersey Avenue, New Brunswick, NJ 08901, USA" +}, { + "location": { + "latitude": 409642566, + "longitude": -746017679 + }, + "name": "6 East Emerald Isle Drive, Lake Hopatcong, NJ 07849, USA" +}, { + "location": { + "latitude": 408031728, + "longitude": -748645385 + }, + "name": "1358-1474 New Jersey 57, Port Murray, NJ 07865, USA" +}, { + "location": { + "latitude": 413700272, + "longitude": -742135189 + }, + "name": "367 Prospect Road, Chester, NY 10918, USA" +}, { + "location": { + "latitude": 404310607, + "longitude": -740282632 + }, + "name": "10 Simon Lake Drive, Atlantic Highlands, NJ 07716, USA" +}, { + "location": { + "latitude": 409319800, + "longitude": -746201391 + }, + "name": "11 Ward Street, Mount Arlington, NJ 07856, USA" +}, { + "location": { + "latitude": 406685311, + "longitude": -742108603 + }, + "name": "300-398 Jefferson Avenue, Elizabeth, NJ 07201, USA" +}, { + "location": { + "latitude": 419018117, + "longitude": -749142781 + }, + "name": "43 Dreher Road, Roscoe, NY 12776, USA" +}, { + "location": { + "latitude": 412856162, + "longitude": -745148837 + }, + "name": "Swan Street, Pine Island, NY 10969, USA" +}, { + "location": { + "latitude": 416560744, + "longitude": -746721964 + }, + "name": "66 Pleasantview Avenue, Monticello, NY 12701, USA" +}, { + "location": { + "latitude": 405314270, + "longitude": -749836354 + }, + "name": "" +}, { + "location": { + "latitude": 414219548, + "longitude": -743327440 + }, + "name": "" +}, { + "location": { + "latitude": 415534177, + "longitude": -742900616 + }, + "name": "565 Winding Hills Road, Montgomery, NY 12549, USA" +}, { + "location": { + "latitude": 406898530, + "longitude": -749127080 + }, + "name": "231 Rocky Run Road, Glen Gardner, NJ 08826, USA" +}, { + "location": { + "latitude": 407586880, + "longitude": -741670168 + }, + "name": "100 Mount Pleasant Avenue, Newark, NJ 07104, USA" +}, { + "location": { + "latitude": 400106455, + "longitude": -742870190 + }, + "name": "517-521 Huntington Drive, Manchester Township, NJ 08759, USA" +}, { + "location": { + "latitude": 400066188, + "longitude": -746793294 + }, + "name": "" +}, { + "location": { + "latitude": 418803880, + "longitude": -744102673 + }, + "name": "40 Mountain Road, Napanoch, NY 12458, USA" +}, { + "location": { + "latitude": 414204288, + "longitude": -747895140 + }, + "name": "" +}, { + "location": { + "latitude": 414777405, + "longitude": -740615601 + }, + "name": "" +}, { + "location": { + "latitude": 415464475, + "longitude": -747175374 + }, + "name": "48 North Road, Forestburgh, NY 12777, USA" +}, { + "location": { + "latitude": 404062378, + "longitude": -746376177 + }, + "name": "" +}, { + "location": { + "latitude": 405688272, + "longitude": -749285130 + }, + "name": "" +}, { + "location": { + "latitude": 400342070, + "longitude": -748788996 + }, + "name": "" +}, { + "location": { + "latitude": 401809022, + "longitude": -744157964 + }, + "name": "" +}, { + "location": { + "latitude": 404226644, + "longitude": -740517141 + }, + "name": "9 Thompson Avenue, Leonardo, NJ 07737, USA" +}, { + "location": { + "latitude": 410322033, + "longitude": -747871659 + }, + "name": "" +}, { + "location": { + "latitude": 407100674, + "longitude": -747742727 + }, + "name": "" +}, { + "location": { + "latitude": 418811433, + "longitude": -741718005 + }, + "name": "213 Bush Road, Stone Ridge, NY 12484, USA" +}, { + "location": { + "latitude": 415034302, + "longitude": -743850945 + }, + "name": "" +}, { + "location": { + "latitude": 411349992, + "longitude": -743694161 + }, + "name": "" +}, { + "location": { + "latitude": 404839914, + "longitude": -744759616 + }, + "name": "1-17 Bergen Court, New Brunswick, NJ 08901, USA" +}, { + "location": { + "latitude": 414638017, + "longitude": -745957854 + }, + "name": "35 Oakland Valley Road, Cuddebackville, NY 12729, USA" +}, { + "location": { + "latitude": 412127800, + "longitude": -740173578 + }, + "name": "" +}, { + "location": { + "latitude": 401263460, + "longitude": -747964303 + }, + "name": "" +}, { + "location": { + "latitude": 412843391, + "longitude": -749086026 + }, + "name": "" +}, { + "location": { + "latitude": 418512773, + "longitude": -743067823 + }, + "name": "" +}, { + "location": { + "latitude": 404318328, + "longitude": -740835638 + }, + "name": "42-102 Main Street, Belford, NJ 07718, USA" +}, { + "location": { + "latitude": 419020746, + "longitude": -741172328 + }, + "name": "" +}, { + "location": { + "latitude": 404080723, + "longitude": -746119569 + }, + "name": "" +}, { + "location": { + "latitude": 401012643, + "longitude": -744035134 + }, + "name": "" +}, { + "location": { + "latitude": 404306372, + "longitude": -741079661 + }, + "name": "" +}, { + "location": { + "latitude": 403966326, + "longitude": -748519297 + }, + "name": "" +}, { + "location": { + "latitude": 405002031, + "longitude": -748407866 + }, + "name": "" +}, { + "location": { + "latitude": 409532885, + "longitude": -742200683 + }, + "name": "" +}, { + "location": { + "latitude": 416851321, + "longitude": -742674555 + }, + "name": "" +}, { + "location": { + "latitude": 406411633, + "longitude": -741722051 + }, + "name": "3387 Richmond Terrace, Staten Island, NY 10303, USA" +}, { + "location": { + "latitude": 413069058, + "longitude": -744597778 + }, + "name": "261 Van Sickle Road, Goshen, NY 10924, USA" +}, { + "location": { + "latitude": 418465462, + "longitude": -746859398 + }, + "name": "" +}, { + "location": { + "latitude": 411733222, + "longitude": -744228360 + }, + "name": "" +}, { + "location": { + "latitude": 410248224, + "longitude": -747127767 + }, + "name": "3 Hasta Way, Newton, NJ 07860, USA" +}]
diff --git a/grpc/example/route_guide/lib/src/client.dart b/grpc/example/route_guide/lib/src/client.dart new file mode 100644 index 0000000..f2ba669 --- /dev/null +++ b/grpc/example/route_guide/lib/src/client.dart
@@ -0,0 +1,148 @@ +// Copyright (c) 2017, the gRPC project authors. Please see the AUTHORS file +// for details. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import 'dart:math' show Random; + +import 'package:grpc/grpc.dart'; + +import 'common.dart'; +import 'generated/route_guide.pb.dart'; +import 'generated/route_guide.pbgrpc.dart'; + +class Client { + RouteGuideClient stub; + + Future<void> main(List<String> args) async { + final channel = ClientChannel('127.0.0.1', + port: 8080, + options: + const ChannelOptions(credentials: ChannelCredentials.insecure())); + stub = RouteGuideClient(channel, + options: CallOptions(timeout: Duration(seconds: 30))); + // Run all of the demos in order. + try { + await runGetFeature(); + await runListFeatures(); + await runRecordRoute(); + await runRouteChat(); + } catch (e) { + print('Caught error: $e'); + } + await channel.shutdown(); + } + + void printFeature(Feature feature) { + final latitude = feature.location.latitude; + final longitude = feature.location.longitude; + final name = feature.name.isEmpty + ? 'no feature' + : 'feature called "${feature.name}"'; + print( + 'Found $name at ${latitude / coordFactor}, ${longitude / coordFactor}'); + } + + /// Run the getFeature demo. Calls getFeature with a point known to have a + /// feature and a point known not to have a feature. + Future<void> runGetFeature() async { + final point1 = Point() + ..latitude = 409146138 + ..longitude = -746188906; + final point2 = Point() + ..latitude = 0 + ..longitude = 0; + + printFeature(await stub.getFeature(point1)); + printFeature(await stub.getFeature(point2)); + } + + /// Run the listFeatures demo. Calls listFeatures with a rectangle containing + /// all of the features in the pre-generated database. Prints each response as + /// it comes in. + Future<void> runListFeatures() async { + final lo = Point() + ..latitude = 400000000 + ..longitude = -750000000; + final hi = Point() + ..latitude = 420000000 + ..longitude = -730000000; + final rect = Rectangle() + ..lo = lo + ..hi = hi; + + print('Looking for features between 40, -75 and 42, -73'); + await for (var feature in stub.listFeatures(rect)) { + printFeature(feature); + } + } + + /// Run the recordRoute demo. Sends several randomly chosen points from the + /// pre-generated feature database with a variable delay in between. Prints + /// the statistics when they are sent from the server. + Future<void> runRecordRoute() async { + Stream<Point> generateRoute(int count) async* { + final random = Random(); + + for (var i = 0; i < count; i++) { + final point = featuresDb[random.nextInt(featuresDb.length)].location; + print( + 'Visiting point ${point.latitude / coordFactor}, ${point.longitude / coordFactor}'); + yield point; + await Future.delayed(Duration(milliseconds: 200 + random.nextInt(100))); + } + } + + final summary = await stub.recordRoute(generateRoute(10)); + print('Finished trip with ${summary.pointCount} points'); + print('Passed ${summary.featureCount} features'); + print('Travelled ${summary.distance} meters'); + print('It took ${summary.elapsedTime} seconds'); + } + + /// Run the routeChat demo. Send some chat messages, and print any chat + /// messages that are sent from the server. + Future<void> runRouteChat() async { + RouteNote createNote(String message, int latitude, int longitude) { + final location = Point() + ..latitude = latitude + ..longitude = longitude; + return RouteNote() + ..message = message + ..location = location; + } + + final notes = <RouteNote>[ + createNote('First message', 0, 0), + createNote('Second message', 0, 1), + createNote('Third message', 1, 0), + createNote('Fourth message', 0, 0), + ]; + + Stream<RouteNote> outgoingNotes() async* { + for (final note in notes) { + // Short delay to simulate some other interaction. + await Future.delayed(Duration(milliseconds: 10)); + print('Sending message ${note.message} at ${note.location.latitude}, ' + '${note.location.longitude}'); + yield note; + } + } + + final call = stub.routeChat(outgoingNotes()); + await for (var note in call) { + print( + 'Got message ${note.message} at ${note.location.latitude}, ${note.location.longitude}'); + } + } +}
diff --git a/grpc/example/route_guide/lib/src/common.dart b/grpc/example/route_guide/lib/src/common.dart new file mode 100644 index 0000000..9daa2a4 --- /dev/null +++ b/grpc/example/route_guide/lib/src/common.dart
@@ -0,0 +1,36 @@ +// Copyright (c) 2017, the gRPC project authors. Please see the AUTHORS file +// for details. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import 'dart:convert'; +import 'dart:io'; + +import 'generated/route_guide.pb.dart'; + +const coordFactor = 1e7; + +final List<Feature> featuresDb = _readDatabase(); + +List<Feature> _readDatabase() { + final dbData = File('data/route_guide_db.json').readAsStringSync(); + final List db = jsonDecode(dbData); + return db.map((entry) { + final location = Point() + ..latitude = entry['location']['latitude'] + ..longitude = entry['location']['longitude']; + return Feature() + ..name = entry['name'] + ..location = location; + }).toList(); +}
diff --git a/grpc/example/route_guide/lib/src/generated/route_guide.pb.dart b/grpc/example/route_guide/lib/src/generated/route_guide.pb.dart new file mode 100644 index 0000000..b18a663 --- /dev/null +++ b/grpc/example/route_guide/lib/src/generated/route_guide.pb.dart
@@ -0,0 +1,505 @@ +/// +// Generated code. Do not modify. +// source: route_guide.proto +// +// @dart = 2.7 +// ignore_for_file: annotate_overrides,camel_case_types,unnecessary_const,non_constant_identifier_names,library_prefixes,unused_import,unused_shown_name,return_of_invalid_type,unnecessary_this,prefer_final_fields + +import 'dart:core' as $core; + +import 'package:protobuf/protobuf.dart' as $pb; + +class Point extends $pb.GeneratedMessage { + static final $pb.BuilderInfo _i = $pb.BuilderInfo( + const $core.bool.fromEnvironment('protobuf.omit_message_names') + ? '' + : 'Point', + package: const $pb.PackageName( + const $core.bool.fromEnvironment('protobuf.omit_message_names') + ? '' + : 'routeguide'), + createEmptyInstance: create) + ..a<$core.int>( + 1, + const $core.bool.fromEnvironment('protobuf.omit_field_names') + ? '' + : 'latitude', + $pb.PbFieldType.O3) + ..a<$core.int>( + 2, + const $core.bool.fromEnvironment('protobuf.omit_field_names') + ? '' + : 'longitude', + $pb.PbFieldType.O3) + ..hasRequiredFields = false; + + Point._() : super(); + factory Point({ + $core.int latitude, + $core.int longitude, + }) { + final _result = create(); + if (latitude != null) { + _result.latitude = latitude; + } + if (longitude != null) { + _result.longitude = longitude; + } + return _result; + } + factory Point.fromBuffer($core.List<$core.int> i, + [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromBuffer(i, r); + factory Point.fromJson($core.String i, + [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromJson(i, r); + @$core.Deprecated('Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.deepCopy] instead. ' + 'Will be removed in next major version') + Point clone() => Point()..mergeFromMessage(this); + @$core.Deprecated('Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.rebuild] instead. ' + 'Will be removed in next major version') + Point copyWith(void Function(Point) updates) => super.copyWith( + (message) => updates(message as Point)); // ignore: deprecated_member_use + $pb.BuilderInfo get info_ => _i; + @$core.pragma('dart2js:noInline') + static Point create() => Point._(); + Point createEmptyInstance() => create(); + static $pb.PbList<Point> createRepeated() => $pb.PbList<Point>(); + @$core.pragma('dart2js:noInline') + static Point getDefault() => + _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor<Point>(create); + static Point _defaultInstance; + + @$pb.TagNumber(1) + $core.int get latitude => $_getIZ(0); + @$pb.TagNumber(1) + set latitude($core.int v) { + $_setSignedInt32(0, v); + } + + @$pb.TagNumber(1) + $core.bool hasLatitude() => $_has(0); + @$pb.TagNumber(1) + void clearLatitude() => clearField(1); + + @$pb.TagNumber(2) + $core.int get longitude => $_getIZ(1); + @$pb.TagNumber(2) + set longitude($core.int v) { + $_setSignedInt32(1, v); + } + + @$pb.TagNumber(2) + $core.bool hasLongitude() => $_has(1); + @$pb.TagNumber(2) + void clearLongitude() => clearField(2); +} + +class Rectangle extends $pb.GeneratedMessage { + static final $pb.BuilderInfo _i = $pb.BuilderInfo( + const $core.bool.fromEnvironment('protobuf.omit_message_names') + ? '' + : 'Rectangle', + package: const $pb.PackageName( + const $core.bool.fromEnvironment('protobuf.omit_message_names') + ? '' + : 'routeguide'), + createEmptyInstance: create) + ..aOM<Point>( + 1, + const $core.bool.fromEnvironment('protobuf.omit_field_names') + ? '' + : 'lo', + subBuilder: Point.create) + ..aOM<Point>( + 2, + const $core.bool.fromEnvironment('protobuf.omit_field_names') + ? '' + : 'hi', + subBuilder: Point.create) + ..hasRequiredFields = false; + + Rectangle._() : super(); + factory Rectangle({ + Point lo, + Point hi, + }) { + final _result = create(); + if (lo != null) { + _result.lo = lo; + } + if (hi != null) { + _result.hi = hi; + } + return _result; + } + factory Rectangle.fromBuffer($core.List<$core.int> i, + [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromBuffer(i, r); + factory Rectangle.fromJson($core.String i, + [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromJson(i, r); + @$core.Deprecated('Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.deepCopy] instead. ' + 'Will be removed in next major version') + Rectangle clone() => Rectangle()..mergeFromMessage(this); + @$core.Deprecated('Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.rebuild] instead. ' + 'Will be removed in next major version') + Rectangle copyWith(void Function(Rectangle) updates) => + super.copyWith((message) => + updates(message as Rectangle)); // ignore: deprecated_member_use + $pb.BuilderInfo get info_ => _i; + @$core.pragma('dart2js:noInline') + static Rectangle create() => Rectangle._(); + Rectangle createEmptyInstance() => create(); + static $pb.PbList<Rectangle> createRepeated() => $pb.PbList<Rectangle>(); + @$core.pragma('dart2js:noInline') + static Rectangle getDefault() => + _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor<Rectangle>(create); + static Rectangle _defaultInstance; + + @$pb.TagNumber(1) + Point get lo => $_getN(0); + @$pb.TagNumber(1) + set lo(Point v) { + setField(1, v); + } + + @$pb.TagNumber(1) + $core.bool hasLo() => $_has(0); + @$pb.TagNumber(1) + void clearLo() => clearField(1); + @$pb.TagNumber(1) + Point ensureLo() => $_ensure(0); + + @$pb.TagNumber(2) + Point get hi => $_getN(1); + @$pb.TagNumber(2) + set hi(Point v) { + setField(2, v); + } + + @$pb.TagNumber(2) + $core.bool hasHi() => $_has(1); + @$pb.TagNumber(2) + void clearHi() => clearField(2); + @$pb.TagNumber(2) + Point ensureHi() => $_ensure(1); +} + +class Feature extends $pb.GeneratedMessage { + static final $pb.BuilderInfo _i = $pb.BuilderInfo( + const $core.bool.fromEnvironment('protobuf.omit_message_names') + ? '' + : 'Feature', + package: const $pb.PackageName( + const $core.bool.fromEnvironment('protobuf.omit_message_names') + ? '' + : 'routeguide'), + createEmptyInstance: create) + ..aOS( + 1, + const $core.bool.fromEnvironment('protobuf.omit_field_names') + ? '' + : 'name') + ..aOM<Point>( + 2, + const $core.bool.fromEnvironment('protobuf.omit_field_names') + ? '' + : 'location', + subBuilder: Point.create) + ..hasRequiredFields = false; + + Feature._() : super(); + factory Feature({ + $core.String name, + Point location, + }) { + final _result = create(); + if (name != null) { + _result.name = name; + } + if (location != null) { + _result.location = location; + } + return _result; + } + factory Feature.fromBuffer($core.List<$core.int> i, + [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromBuffer(i, r); + factory Feature.fromJson($core.String i, + [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromJson(i, r); + @$core.Deprecated('Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.deepCopy] instead. ' + 'Will be removed in next major version') + Feature clone() => Feature()..mergeFromMessage(this); + @$core.Deprecated('Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.rebuild] instead. ' + 'Will be removed in next major version') + Feature copyWith(void Function(Feature) updates) => + super.copyWith((message) => + updates(message as Feature)); // ignore: deprecated_member_use + $pb.BuilderInfo get info_ => _i; + @$core.pragma('dart2js:noInline') + static Feature create() => Feature._(); + Feature createEmptyInstance() => create(); + static $pb.PbList<Feature> createRepeated() => $pb.PbList<Feature>(); + @$core.pragma('dart2js:noInline') + static Feature getDefault() => + _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor<Feature>(create); + static Feature _defaultInstance; + + @$pb.TagNumber(1) + $core.String get name => $_getSZ(0); + @$pb.TagNumber(1) + set name($core.String v) { + $_setString(0, v); + } + + @$pb.TagNumber(1) + $core.bool hasName() => $_has(0); + @$pb.TagNumber(1) + void clearName() => clearField(1); + + @$pb.TagNumber(2) + Point get location => $_getN(1); + @$pb.TagNumber(2) + set location(Point v) { + setField(2, v); + } + + @$pb.TagNumber(2) + $core.bool hasLocation() => $_has(1); + @$pb.TagNumber(2) + void clearLocation() => clearField(2); + @$pb.TagNumber(2) + Point ensureLocation() => $_ensure(1); +} + +class RouteNote extends $pb.GeneratedMessage { + static final $pb.BuilderInfo _i = $pb.BuilderInfo( + const $core.bool.fromEnvironment('protobuf.omit_message_names') + ? '' + : 'RouteNote', + package: const $pb.PackageName( + const $core.bool.fromEnvironment('protobuf.omit_message_names') + ? '' + : 'routeguide'), + createEmptyInstance: create) + ..aOM<Point>( + 1, + const $core.bool.fromEnvironment('protobuf.omit_field_names') + ? '' + : 'location', + subBuilder: Point.create) + ..aOS( + 2, + const $core.bool.fromEnvironment('protobuf.omit_field_names') + ? '' + : 'message') + ..hasRequiredFields = false; + + RouteNote._() : super(); + factory RouteNote({ + Point location, + $core.String message, + }) { + final _result = create(); + if (location != null) { + _result.location = location; + } + if (message != null) { + _result.message = message; + } + return _result; + } + factory RouteNote.fromBuffer($core.List<$core.int> i, + [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromBuffer(i, r); + factory RouteNote.fromJson($core.String i, + [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromJson(i, r); + @$core.Deprecated('Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.deepCopy] instead. ' + 'Will be removed in next major version') + RouteNote clone() => RouteNote()..mergeFromMessage(this); + @$core.Deprecated('Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.rebuild] instead. ' + 'Will be removed in next major version') + RouteNote copyWith(void Function(RouteNote) updates) => + super.copyWith((message) => + updates(message as RouteNote)); // ignore: deprecated_member_use + $pb.BuilderInfo get info_ => _i; + @$core.pragma('dart2js:noInline') + static RouteNote create() => RouteNote._(); + RouteNote createEmptyInstance() => create(); + static $pb.PbList<RouteNote> createRepeated() => $pb.PbList<RouteNote>(); + @$core.pragma('dart2js:noInline') + static RouteNote getDefault() => + _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor<RouteNote>(create); + static RouteNote _defaultInstance; + + @$pb.TagNumber(1) + Point get location => $_getN(0); + @$pb.TagNumber(1) + set location(Point v) { + setField(1, v); + } + + @$pb.TagNumber(1) + $core.bool hasLocation() => $_has(0); + @$pb.TagNumber(1) + void clearLocation() => clearField(1); + @$pb.TagNumber(1) + Point ensureLocation() => $_ensure(0); + + @$pb.TagNumber(2) + $core.String get message => $_getSZ(1); + @$pb.TagNumber(2) + set message($core.String v) { + $_setString(1, v); + } + + @$pb.TagNumber(2) + $core.bool hasMessage() => $_has(1); + @$pb.TagNumber(2) + void clearMessage() => clearField(2); +} + +class RouteSummary extends $pb.GeneratedMessage { + static final $pb.BuilderInfo _i = $pb.BuilderInfo( + const $core.bool.fromEnvironment('protobuf.omit_message_names') + ? '' + : 'RouteSummary', + package: const $pb.PackageName( + const $core.bool.fromEnvironment('protobuf.omit_message_names') + ? '' + : 'routeguide'), + createEmptyInstance: create) + ..a<$core.int>( + 1, + const $core.bool.fromEnvironment('protobuf.omit_field_names') + ? '' + : 'pointCount', + $pb.PbFieldType.O3) + ..a<$core.int>( + 2, + const $core.bool.fromEnvironment('protobuf.omit_field_names') + ? '' + : 'featureCount', + $pb.PbFieldType.O3) + ..a<$core.int>( + 3, + const $core.bool.fromEnvironment('protobuf.omit_field_names') + ? '' + : 'distance', + $pb.PbFieldType.O3) + ..a<$core.int>( + 4, + const $core.bool.fromEnvironment('protobuf.omit_field_names') + ? '' + : 'elapsedTime', + $pb.PbFieldType.O3) + ..hasRequiredFields = false; + + RouteSummary._() : super(); + factory RouteSummary({ + $core.int pointCount, + $core.int featureCount, + $core.int distance, + $core.int elapsedTime, + }) { + final _result = create(); + if (pointCount != null) { + _result.pointCount = pointCount; + } + if (featureCount != null) { + _result.featureCount = featureCount; + } + if (distance != null) { + _result.distance = distance; + } + if (elapsedTime != null) { + _result.elapsedTime = elapsedTime; + } + return _result; + } + factory RouteSummary.fromBuffer($core.List<$core.int> i, + [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromBuffer(i, r); + factory RouteSummary.fromJson($core.String i, + [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromJson(i, r); + @$core.Deprecated('Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.deepCopy] instead. ' + 'Will be removed in next major version') + RouteSummary clone() => RouteSummary()..mergeFromMessage(this); + @$core.Deprecated('Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.rebuild] instead. ' + 'Will be removed in next major version') + RouteSummary copyWith(void Function(RouteSummary) updates) => + super.copyWith((message) => + updates(message as RouteSummary)); // ignore: deprecated_member_use + $pb.BuilderInfo get info_ => _i; + @$core.pragma('dart2js:noInline') + static RouteSummary create() => RouteSummary._(); + RouteSummary createEmptyInstance() => create(); + static $pb.PbList<RouteSummary> createRepeated() => + $pb.PbList<RouteSummary>(); + @$core.pragma('dart2js:noInline') + static RouteSummary getDefault() => _defaultInstance ??= + $pb.GeneratedMessage.$_defaultFor<RouteSummary>(create); + static RouteSummary _defaultInstance; + + @$pb.TagNumber(1) + $core.int get pointCount => $_getIZ(0); + @$pb.TagNumber(1) + set pointCount($core.int v) { + $_setSignedInt32(0, v); + } + + @$pb.TagNumber(1) + $core.bool hasPointCount() => $_has(0); + @$pb.TagNumber(1) + void clearPointCount() => clearField(1); + + @$pb.TagNumber(2) + $core.int get featureCount => $_getIZ(1); + @$pb.TagNumber(2) + set featureCount($core.int v) { + $_setSignedInt32(1, v); + } + + @$pb.TagNumber(2) + $core.bool hasFeatureCount() => $_has(1); + @$pb.TagNumber(2) + void clearFeatureCount() => clearField(2); + + @$pb.TagNumber(3) + $core.int get distance => $_getIZ(2); + @$pb.TagNumber(3) + set distance($core.int v) { + $_setSignedInt32(2, v); + } + + @$pb.TagNumber(3) + $core.bool hasDistance() => $_has(2); + @$pb.TagNumber(3) + void clearDistance() => clearField(3); + + @$pb.TagNumber(4) + $core.int get elapsedTime => $_getIZ(3); + @$pb.TagNumber(4) + set elapsedTime($core.int v) { + $_setSignedInt32(3, v); + } + + @$pb.TagNumber(4) + $core.bool hasElapsedTime() => $_has(3); + @$pb.TagNumber(4) + void clearElapsedTime() => clearField(4); +}
diff --git a/grpc/example/route_guide/lib/src/generated/route_guide.pbenum.dart b/grpc/example/route_guide/lib/src/generated/route_guide.pbenum.dart new file mode 100644 index 0000000..a510b01 --- /dev/null +++ b/grpc/example/route_guide/lib/src/generated/route_guide.pbenum.dart
@@ -0,0 +1,6 @@ +/// +// Generated code. Do not modify. +// source: route_guide.proto +// +// @dart = 2.7 +// ignore_for_file: annotate_overrides,camel_case_types,unnecessary_const,non_constant_identifier_names,library_prefixes,unused_import,unused_shown_name,return_of_invalid_type,unnecessary_this,prefer_final_fields
diff --git a/grpc/example/route_guide/lib/src/generated/route_guide.pbgrpc.dart b/grpc/example/route_guide/lib/src/generated/route_guide.pbgrpc.dart new file mode 100644 index 0000000..5e0d2f0 --- /dev/null +++ b/grpc/example/route_guide/lib/src/generated/route_guide.pbgrpc.dart
@@ -0,0 +1,117 @@ +/// +// Generated code. Do not modify. +// source: route_guide.proto +// +// @dart = 2.7 +// ignore_for_file: annotate_overrides,camel_case_types,unnecessary_const,non_constant_identifier_names,library_prefixes,unused_import,unused_shown_name,return_of_invalid_type,unnecessary_this,prefer_final_fields + +import 'dart:async' as $async; + +import 'dart:core' as $core; + +import 'package:grpc/service_api.dart' as $grpc; +import 'route_guide.pb.dart' as $0; +export 'route_guide.pb.dart'; + +class RouteGuideClient extends $grpc.Client { + static final _$getFeature = $grpc.ClientMethod<$0.Point, $0.Feature>( + '/routeguide.RouteGuide/GetFeature', + ($0.Point value) => value.writeToBuffer(), + ($core.List<$core.int> value) => $0.Feature.fromBuffer(value)); + static final _$listFeatures = $grpc.ClientMethod<$0.Rectangle, $0.Feature>( + '/routeguide.RouteGuide/ListFeatures', + ($0.Rectangle value) => value.writeToBuffer(), + ($core.List<$core.int> value) => $0.Feature.fromBuffer(value)); + static final _$recordRoute = $grpc.ClientMethod<$0.Point, $0.RouteSummary>( + '/routeguide.RouteGuide/RecordRoute', + ($0.Point value) => value.writeToBuffer(), + ($core.List<$core.int> value) => $0.RouteSummary.fromBuffer(value)); + static final _$routeChat = $grpc.ClientMethod<$0.RouteNote, $0.RouteNote>( + '/routeguide.RouteGuide/RouteChat', + ($0.RouteNote value) => value.writeToBuffer(), + ($core.List<$core.int> value) => $0.RouteNote.fromBuffer(value)); + + RouteGuideClient($grpc.ClientChannel channel, + {$grpc.CallOptions options, + $core.Iterable<$grpc.ClientInterceptor> interceptors}) + : super(channel, options: options, interceptors: interceptors); + + $grpc.ResponseFuture<$0.Feature> getFeature($0.Point request, + {$grpc.CallOptions options}) { + return $createUnaryCall(_$getFeature, request, options: options); + } + + $grpc.ResponseStream<$0.Feature> listFeatures($0.Rectangle request, + {$grpc.CallOptions options}) { + return $createStreamingCall( + _$listFeatures, $async.Stream.fromIterable([request]), + options: options); + } + + $grpc.ResponseFuture<$0.RouteSummary> recordRoute( + $async.Stream<$0.Point> request, + {$grpc.CallOptions options}) { + return $createStreamingCall(_$recordRoute, request, options: options) + .single; + } + + $grpc.ResponseStream<$0.RouteNote> routeChat( + $async.Stream<$0.RouteNote> request, + {$grpc.CallOptions options}) { + return $createStreamingCall(_$routeChat, request, options: options); + } +} + +abstract class RouteGuideServiceBase extends $grpc.Service { + $core.String get $name => 'routeguide.RouteGuide'; + + RouteGuideServiceBase() { + $addMethod($grpc.ServiceMethod<$0.Point, $0.Feature>( + 'GetFeature', + getFeature_Pre, + false, + false, + ($core.List<$core.int> value) => $0.Point.fromBuffer(value), + ($0.Feature value) => value.writeToBuffer())); + $addMethod($grpc.ServiceMethod<$0.Rectangle, $0.Feature>( + 'ListFeatures', + listFeatures_Pre, + false, + true, + ($core.List<$core.int> value) => $0.Rectangle.fromBuffer(value), + ($0.Feature value) => value.writeToBuffer())); + $addMethod($grpc.ServiceMethod<$0.Point, $0.RouteSummary>( + 'RecordRoute', + recordRoute, + true, + false, + ($core.List<$core.int> value) => $0.Point.fromBuffer(value), + ($0.RouteSummary value) => value.writeToBuffer())); + $addMethod($grpc.ServiceMethod<$0.RouteNote, $0.RouteNote>( + 'RouteChat', + routeChat, + true, + true, + ($core.List<$core.int> value) => $0.RouteNote.fromBuffer(value), + ($0.RouteNote value) => value.writeToBuffer())); + } + + $async.Future<$0.Feature> getFeature_Pre( + $grpc.ServiceCall call, $async.Future<$0.Point> request) async { + return getFeature(call, await request); + } + + $async.Stream<$0.Feature> listFeatures_Pre( + $grpc.ServiceCall call, $async.Future<$0.Rectangle> request) async* { + yield* listFeatures(call, await request); + } + + $async.Future<$0.Feature> getFeature( + $grpc.ServiceCall call, $0.Point request); + $async.Stream<$0.Feature> listFeatures( + $grpc.ServiceCall call, $0.Rectangle request); + $async.Future<$0.RouteSummary> recordRoute( + $grpc.ServiceCall call, $async.Stream<$0.Point> request); + $async.Stream<$0.RouteNote> routeChat( + $grpc.ServiceCall call, $async.Stream<$0.RouteNote> request); +}
diff --git a/grpc/example/route_guide/lib/src/generated/route_guide.pbjson.dart b/grpc/example/route_guide/lib/src/generated/route_guide.pbjson.dart new file mode 100644 index 0000000..687d9a6 --- /dev/null +++ b/grpc/example/route_guide/lib/src/generated/route_guide.pbjson.dart
@@ -0,0 +1,76 @@ +/// +// Generated code. Do not modify. +// source: route_guide.proto +// +// @dart = 2.7 +// ignore_for_file: annotate_overrides,camel_case_types,unnecessary_const,non_constant_identifier_names,library_prefixes,unused_import,unused_shown_name,return_of_invalid_type,unnecessary_this,prefer_final_fields + +const Point$json = const { + '1': 'Point', + '2': const [ + const {'1': 'latitude', '3': 1, '4': 1, '5': 5, '10': 'latitude'}, + const {'1': 'longitude', '3': 2, '4': 1, '5': 5, '10': 'longitude'}, + ], +}; + +const Rectangle$json = const { + '1': 'Rectangle', + '2': const [ + const { + '1': 'lo', + '3': 1, + '4': 1, + '5': 11, + '6': '.routeguide.Point', + '10': 'lo' + }, + const { + '1': 'hi', + '3': 2, + '4': 1, + '5': 11, + '6': '.routeguide.Point', + '10': 'hi' + }, + ], +}; + +const Feature$json = const { + '1': 'Feature', + '2': const [ + const {'1': 'name', '3': 1, '4': 1, '5': 9, '10': 'name'}, + const { + '1': 'location', + '3': 2, + '4': 1, + '5': 11, + '6': '.routeguide.Point', + '10': 'location' + }, + ], +}; + +const RouteNote$json = const { + '1': 'RouteNote', + '2': const [ + const { + '1': 'location', + '3': 1, + '4': 1, + '5': 11, + '6': '.routeguide.Point', + '10': 'location' + }, + const {'1': 'message', '3': 2, '4': 1, '5': 9, '10': 'message'}, + ], +}; + +const RouteSummary$json = const { + '1': 'RouteSummary', + '2': const [ + const {'1': 'point_count', '3': 1, '4': 1, '5': 5, '10': 'pointCount'}, + const {'1': 'feature_count', '3': 2, '4': 1, '5': 5, '10': 'featureCount'}, + const {'1': 'distance', '3': 3, '4': 1, '5': 5, '10': 'distance'}, + const {'1': 'elapsed_time', '3': 4, '4': 1, '5': 5, '10': 'elapsedTime'}, + ], +};
diff --git a/grpc/example/route_guide/lib/src/server.dart b/grpc/example/route_guide/lib/src/server.dart new file mode 100644 index 0000000..74782d5 --- /dev/null +++ b/grpc/example/route_guide/lib/src/server.dart
@@ -0,0 +1,151 @@ +// Copyright (c) 2017, the gRPC project authors. Please see the AUTHORS file +// for details. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import 'dart:math' show atan2, cos, max, min, pi, sin, sqrt; + +import 'package:grpc/grpc.dart' as grpc; + +import 'common.dart'; +import 'generated/route_guide.pb.dart'; +import 'generated/route_guide.pbgrpc.dart'; + +class RouteGuideService extends RouteGuideServiceBase { + final routeNotes = <Point, List<RouteNote>>{}; + + /// GetFeature handler. Returns a feature for the given location. + /// The [context] object provides access to client metadata, cancellation, etc. + @override + Future<Feature> getFeature(grpc.ServiceCall call, Point request) async { + return featuresDb.firstWhere((f) => f.location == request, + orElse: () => Feature()..location = request); + } + + Rectangle _normalize(Rectangle r) { + final lo = Point() + ..latitude = min(r.lo.latitude, r.hi.latitude) + ..longitude = min(r.lo.longitude, r.hi.longitude); + + final hi = Point() + ..latitude = max(r.lo.latitude, r.hi.latitude) + ..longitude = max(r.lo.longitude, r.hi.longitude); + + return Rectangle() + ..lo = lo + ..hi = hi; + } + + bool _contains(Rectangle r, Point p) { + return p.longitude >= r.lo.longitude && + p.longitude <= r.hi.longitude && + p.latitude >= r.lo.latitude && + p.latitude <= r.hi.latitude; + } + + /// ListFeatures handler. Returns a stream of features within the given + /// rectangle. + @override + Stream<Feature> listFeatures( + grpc.ServiceCall call, Rectangle request) async* { + final normalizedRectangle = _normalize(request); + // For each feature, check if it is in the given bounding box + for (var feature in featuresDb) { + if (feature.name.isEmpty) continue; + final location = feature.location; + if (_contains(normalizedRectangle, location)) { + yield feature; + } + } + } + + /// RecordRoute handler. Gets a stream of points, and responds with statistics + /// about the "trip": number of points, number of known features visited, + /// total distance traveled, and total time spent. + @override + Future<RouteSummary> recordRoute( + grpc.ServiceCall call, Stream<Point> request) async { + var pointCount = 0; + var featureCount = 0; + var distance = 0.0; + Point previous; + final timer = Stopwatch(); + + await for (var location in request) { + if (!timer.isRunning) timer.start(); + pointCount++; + final feature = featuresDb.firstWhere((f) => f.location == location, + orElse: () => null); + if (feature != null) { + featureCount++; + } + // For each point after the first, add the incremental distance from the + // previous point to the total distance value. + if (previous != null) distance += _distance(previous, location); + previous = location; + } + timer.stop(); + return RouteSummary() + ..pointCount = pointCount + ..featureCount = featureCount + ..distance = distance.round() + ..elapsedTime = timer.elapsed.inSeconds; + } + + /// RouteChat handler. Receives a stream of message/location pairs, and + /// responds with a stream of all previous messages at each of those + /// locations. + @override + Stream<RouteNote> routeChat( + grpc.ServiceCall call, Stream<RouteNote> request) async* { + await for (var note in request) { + final notes = routeNotes.putIfAbsent(note.location, () => <RouteNote>[]); + for (var note in notes) { + yield note; + } + notes.add(note); + } + } + + /// Calculate the distance between two points using the "haversine" formula. + /// This code was taken from http://www.movable-type.co.uk/scripts/latlong.html. + double _distance(Point start, Point end) { + double toRadians(double num) { + return num * pi / 180; + } + + final lat1 = start.latitude / coordFactor; + final lat2 = end.latitude / coordFactor; + final lon1 = start.longitude / coordFactor; + final lon2 = end.longitude / coordFactor; + final R = 6371000; // metres + final phi1 = toRadians(lat1); + final phi2 = toRadians(lat2); + final dLat = toRadians(lat2 - lat1); + final dLon = toRadians(lon2 - lon1); + + final a = sin(dLat / 2) * sin(dLat / 2) + + cos(phi1) * cos(phi2) * sin(dLon / 2) * sin(dLon / 2); + final c = 2 * atan2(sqrt(a), sqrt(1 - a)); + + return R * c; + } +} + +class Server { + Future<void> main(List<String> args) async { + final server = grpc.Server([RouteGuideService()]); + await server.serve(port: 8080); + print('Server listening on port ${server.port}...'); + } +}
diff --git a/grpc/example/route_guide/protos/route_guide.proto b/grpc/example/route_guide/protos/route_guide.proto new file mode 100644 index 0000000..12c4495 --- /dev/null +++ b/grpc/example/route_guide/protos/route_guide.proto
@@ -0,0 +1,126 @@ +// Copyright 2015, Google Inc. +// All rights reserved. +// +// 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 Inc. 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. + +syntax = "proto3"; + +option java_multiple_files = true; +option java_package = "io.grpc.examples.routeguide"; +option java_outer_classname = "RouteGuideProto"; +option objc_class_prefix = "RTG"; + +package routeguide; + +// Interface exported by the server. +service RouteGuide { + // A simple RPC. + // + // Obtains the feature at a given position. + // + // A feature with an empty name is returned if there's no feature at the given + // position. + rpc GetFeature(Point) returns (Feature) {} + + // A server-to-client streaming RPC. + // + // Obtains the Features available within the given Rectangle. Results are + // streamed rather than returned at once (e.g. in a response message with a + // repeated field), as the rectangle may cover a large area and contain a + // huge number of features. + rpc ListFeatures(Rectangle) returns (stream Feature) {} + + // A client-to-server streaming RPC. + // + // Accepts a stream of Points on a route being traversed, returning a + // RouteSummary when traversal is completed. + rpc RecordRoute(stream Point) returns (RouteSummary) {} + + // A Bidirectional streaming RPC. + // + // Accepts a stream of RouteNotes sent while a route is being traversed, + // while receiving other RouteNotes (e.g. from other users). + rpc RouteChat(stream RouteNote) returns (stream RouteNote) {} +} + +// Points are represented as latitude-longitude pairs in the E7 representation +// (degrees multiplied by 10**7 and rounded to the nearest integer). +// Latitudes should be in the range +/- 90 degrees and longitude should be in +// the range +/- 180 degrees (inclusive). +message Point { + int32 latitude = 1; + int32 longitude = 2; +} + +// A latitude-longitude rectangle, represented as two diagonally opposite +// points "lo" and "hi". +message Rectangle { + // One corner of the rectangle. + Point lo = 1; + + // The other corner of the rectangle. + Point hi = 2; +} + +// A feature names something at a given point. +// +// If a feature could not be named, the name is empty. +message Feature { + // The name of the feature. + string name = 1; + + // The point where the feature is detected. + Point location = 2; +} + +// A RouteNote is a message sent while at a given point. +message RouteNote { + // The location from which the message is sent. + Point location = 1; + + // The message to be sent. + string message = 2; +} + +// A RouteSummary is received in response to a RecordRoute rpc. +// +// It contains the number of individual points received, the number of +// detected features, and the total distance covered as the cumulative sum of +// the distance between each point. +message RouteSummary { + // The number of points received. + int32 point_count = 1; + + // The number of known features passed while traversing the route. + int32 feature_count = 2; + + // The distance covered in metres. + int32 distance = 3; + + // The duration of the traversal in seconds. + int32 elapsed_time = 4; +}
diff --git a/grpc/example/route_guide/pubspec.yaml b/grpc/example/route_guide/pubspec.yaml new file mode 100644 index 0000000..2c6e72c --- /dev/null +++ b/grpc/example/route_guide/pubspec.yaml
@@ -0,0 +1,12 @@ +name: route_guide +description: Dart gRPC sample client and server. +publish_to: none + +environment: + sdk: '>=2.11.99 <3.0.0' + +dependencies: + async: ^2.2.0 + grpc: + path: ../../ + protobuf: ^2.0.0
diff --git a/grpc/interop/bin/client.dart b/grpc/interop/bin/client.dart new file mode 100644 index 0000000..e713adb --- /dev/null +++ b/grpc/interop/bin/client.dart
@@ -0,0 +1,116 @@ +// Copyright (c) 2017, the gRPC project authors. Please see the AUTHORS file +// for details. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import 'dart:async'; + +import 'package:args/args.dart'; + +import 'package:interop/src/client.dart'; + +const _serverHostArgument = 'server_host'; +const _serverHostOverrideArgument = 'server_host_override'; +const _serverPortArgument = 'server_port'; +const _testCaseArgument = 'test_case'; +const _useTLSArgument = 'use_tls'; +const _useTestCAArgument = 'use_test_ca'; +const _defaultServiceAccountArgument = 'default_service_account'; +const _oauthScopeArgument = 'oauth_scope'; +const _serviceAccountKeyFileArgument = 'service_account_key_file'; + +/// Clients implement test cases that test certain functionally. Each client is +/// provided the test case it is expected to run as a command-line parameter. +/// Names should be lowercase and without spaces. +/// +/// Clients should accept these arguments: +/// +/// * --server_host=HOSTNAME +/// * The server host to connect to. For example, "localhost" or "127.0.0.1" +/// * --server_host_override=HOSTNAME +/// * The server host to claim to be connecting to, for use in TLS and +/// HTTP/2 :authority header. If unspecified, the value of --server_host +/// will be used +/// * --server_port=PORT +/// * The server port to connect to. For example, "8080" +/// * --test_case=TESTCASE +/// * The name of the test case to execute. For example, "empty_unary" +/// * --use_tls=BOOLEAN +/// * Whether to use a plaintext or encrypted connection +/// * --use_test_ca=BOOLEAN +/// * Whether to replace platform root CAs with ca.pem as the CA root +/// * --default_service_account=ACCOUNT_EMAIL +/// * Email of the GCE default service account. +/// * --oauth_scope=SCOPE +/// * OAuth scope. For example, "https://www.googleapis.com/auth/xapi.zoo" +/// * --service_account_key_file=PATH +/// * The path to the service account JSON key file generated from GCE +/// developer console. +/// +/// Clients must support TLS with ALPN. Clients must not disable certificate +/// checking. +Future<int> main(List<String> args) async { + final argumentParser = ArgParser(); + argumentParser.addOption(_serverHostArgument, + help: 'The server host to connect to. For example, "localhost" or ' + '"127.0.0.1".'); + argumentParser.addOption(_serverHostOverrideArgument, + help: 'The server host to claim to be connecting to, for use in TLS and ' + 'HTTP/2 :authority header. If unspecified, the value of ' + '--server_host will be used.'); + argumentParser.addOption(_serverPortArgument, + help: 'The server port to connect to. For example, "8080".'); + argumentParser.addOption(_testCaseArgument, + help: + 'The name of the test case to execute. For example, "empty_unary".'); + argumentParser.addOption(_useTLSArgument, + defaultsTo: 'false', + help: 'Whether to use a plaintext or encrypted connection.'); + argumentParser.addOption(_useTestCAArgument, + defaultsTo: 'false', + help: 'Whether to replace platform root CAs with ca.pem as the CA root.'); + argumentParser.addOption(_defaultServiceAccountArgument, + help: 'Email of the GCE default service account.'); + argumentParser.addOption(_oauthScopeArgument, + help: 'OAuth scope. For example, ' + '"https://www.googleapis.com/auth/xapi.zoo".'); + argumentParser.addOption(_serviceAccountKeyFileArgument, + help: 'The path to the service account JSON key file generated from GCE ' + 'developer console.'); + final arguments = argumentParser.parse(args); + + late Tester testClient; + try { + testClient = Tester( + serverHost: arguments[_serverHostArgument] ?? + (throw 'Must specify --$_serverHostArgument'), + serverHostOverride: arguments[_serverHostOverrideArgument], + serverPort: int.tryParse(arguments[_serverPortArgument] ?? + (throw 'Must specify --$_serverPortArgument')) ?? + (throw 'Invalid port "${arguments[_serverPortArgument]}"'), + testCase: arguments[_testCaseArgument] ?? + (throw 'Must specify --$_testCaseArgument'), + useTls: arguments[_useTLSArgument] == 'true', + useTestCA: arguments[_useTestCAArgument] == 'true', + defaultServiceAccount: arguments[_defaultServiceAccountArgument], + oauthScope: arguments[_oauthScopeArgument], + serviceAccountKeyFile: arguments[_serviceAccountKeyFileArgument]); + } catch (e) { + print(e); + print(argumentParser.usage); + return -1; + } + await testClient.runTest(); + print('Passed.'); + return 0; +}
diff --git a/grpc/interop/bin/server.dart b/grpc/interop/bin/server.dart new file mode 100644 index 0000000..a4cf86e --- /dev/null +++ b/grpc/interop/bin/server.dart
@@ -0,0 +1,145 @@ +// Copyright (c) 2017, the gRPC project authors. Please see the AUTHORS file +// for details. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import 'dart:async'; +import 'dart:convert'; +import 'dart:io'; + +import 'package:args/args.dart'; +import 'package:grpc/grpc.dart'; + +import 'package:interop/src/generated/empty.pb.dart'; +import 'package:interop/src/generated/messages.pb.dart'; +import 'package:interop/src/generated/test.pbgrpc.dart'; + +const _headerEchoKey = 'x-grpc-test-echo-initial'; +const _trailerEchoKey = 'x-grpc-test-echo-trailing-bin'; + +class TestService extends TestServiceBase { + @override + void $onMetadata(ServiceCall context) { + final headerEcho = context.clientMetadata![_headerEchoKey]; + if (headerEcho != null) { + context.headers![_headerEchoKey] = headerEcho; + } + final trailerEcho = context.clientMetadata![_trailerEchoKey]; + if (trailerEcho != null) { + context.trailers![_trailerEchoKey] = trailerEcho; + } + } + + @override + Future<Empty> emptyCall(ServiceCall call, Empty request) async { + return Empty(); + } + + @override + Future<SimpleResponse> unaryCall( + ServiceCall call, SimpleRequest request) async { + if (request.responseStatus.code != 0) { + throw GrpcError.custom( + request.responseStatus.code, request.responseStatus.message); + } + final payload = Payload()..body = List.filled(request.responseSize, 0); + return SimpleResponse()..payload = payload; + } + + @override + Future<SimpleResponse> cacheableUnaryCall( + ServiceCall call, SimpleRequest request) async { + final timestamp = DateTime.now().microsecond * 1000; + final responsePayload = Payload()..body = ascii.encode('$timestamp'); + return SimpleResponse()..payload = responsePayload; + } + + @override + Future<StreamingInputCallResponse> streamingInputCall( + ServiceCall call, Stream<StreamingInputCallRequest> request) async { + final aggregatedPayloadSize = await request.fold<int>( + 0, (size, message) => size + message.payload.body.length); + return StreamingInputCallResponse() + ..aggregatedPayloadSize = aggregatedPayloadSize; + } + + Payload _payloadForRequest(ResponseParameters entry) => + Payload()..body = List.filled(entry.size, 0); + + @override + Stream<StreamingOutputCallResponse> streamingOutputCall( + ServiceCall call, StreamingOutputCallRequest request) async* { + for (final entry in request.responseParameters) { + if (entry.intervalUs > 0) { + await Future.delayed(Duration(microseconds: entry.intervalUs)); + } + yield StreamingOutputCallResponse()..payload = _payloadForRequest(entry); + } + } + + StreamingOutputCallResponse _responseForRequest( + StreamingOutputCallRequest request) { + if (request.responseStatus.code != 0) { + throw GrpcError.custom( + request.responseStatus.code, request.responseStatus.message); + } + final response = StreamingOutputCallResponse(); + if (request.responseParameters.isNotEmpty) { + response.payload = _payloadForRequest(request.responseParameters[0]); + } + return response; + } + + @override + Stream<StreamingOutputCallResponse> fullDuplexCall( + ServiceCall call, Stream<StreamingOutputCallRequest> request) async* { + yield* request.map(_responseForRequest); + } + + @override + Stream<StreamingOutputCallResponse> halfDuplexCall( + ServiceCall call, Stream<StreamingOutputCallRequest> request) async* { + final bufferedResponses = await request.map(_responseForRequest).toList(); + yield* Stream.fromIterable(bufferedResponses); + } + + @override + Future<Empty> unimplementedCall(ServiceCall call, Empty request) async { + call.sendTrailers(status: StatusCode.unimplemented); + return Empty(); + } +} + +Future<void> main(List<String> args) async { + final argumentParser = ArgParser(); + argumentParser.addOption('port', defaultsTo: '8080'); + argumentParser.addOption('use_tls', defaultsTo: 'false'); + argumentParser.addOption('tls_cert_file', defaultsTo: 'server1.pem'); + argumentParser.addOption('tls_key_file', defaultsTo: 'server1.key'); + final arguments = argumentParser.parse(args); + final port = int.parse(arguments['port']); + + final services = [TestService()]; + + final server = Server(services); + + ServerTlsCredentials? tlsCredentials; + if (arguments['use_tls'] == 'true') { + final certificate = File(arguments['tls_cert_file']).readAsBytes(); + final privateKey = File(arguments['tls_key_file']).readAsBytes(); + tlsCredentials = ServerTlsCredentials( + certificate: await certificate, privateKey: await privateKey); + } + await server.serve(port: port, security: tlsCredentials); + print('Server listening on port ${server.port}...'); +}
diff --git a/grpc/interop/ca.pem b/grpc/interop/ca.pem new file mode 100644 index 0000000..49d39cd --- /dev/null +++ b/grpc/interop/ca.pem
@@ -0,0 +1,20 @@ +-----BEGIN CERTIFICATE----- +MIIDWjCCAkKgAwIBAgIUWrP0VvHcy+LP6UuYNtiL9gBhD5owDQYJKoZIhvcNAQEL +BQAwVjELMAkGA1UEBhMCQVUxEzARBgNVBAgMClNvbWUtU3RhdGUxITAfBgNVBAoM +GEludGVybmV0IFdpZGdpdHMgUHR5IEx0ZDEPMA0GA1UEAwwGdGVzdGNhMB4XDTIw +MDMxNzE4NTk1MVoXDTMwMDMxNTE4NTk1MVowVjELMAkGA1UEBhMCQVUxEzARBgNV +BAgMClNvbWUtU3RhdGUxITAfBgNVBAoMGEludGVybmV0IFdpZGdpdHMgUHR5IEx0 +ZDEPMA0GA1UEAwwGdGVzdGNhMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKC +AQEAsGL0oXflF0LzoM+Bh+qUU9yhqzw2w8OOX5mu/iNCyUOBrqaHi7mGHx73GD01 +diNzCzvlcQqdNIH6NQSL7DTpBjca66jYT9u73vZe2MDrr1nVbuLvfu9850cdxiUO +Inv5xf8+sTHG0C+a+VAvMhsLiRjsq+lXKRJyk5zkbbsETybqpxoJ+K7CoSy3yc/k +QIY3TipwEtwkKP4hzyo6KiGd/DPexie4nBUInN3bS1BUeNZ5zeaIC2eg3bkeeW7c +qT55b+Yen6CxY0TEkzBK6AKt/WUialKMgT0wbTxRZO7kUCH3Sq6e/wXeFdJ+HvdV +LPlAg5TnMaNpRdQih/8nRFpsdwIDAQABoyAwHjAMBgNVHRMEBTADAQH/MA4GA1Ud +DwEB/wQEAwICBDANBgkqhkiG9w0BAQsFAAOCAQEAkTrKZjBrJXHps/HrjNCFPb5a +THuGPCSsepe1wkKdSp1h4HGRpLoCgcLysCJ5hZhRpHkRihhef+rFHEe60UePQO3S +CVTtdJB4CYWpcNyXOdqefrbJW5QNljxgi6Fhvs7JJkBqdXIkWXtFk2eRgOIP2Eo9 +/OHQHlYnwZFrk6sp4wPyR+A95S0toZBcyDVz7u+hOW0pGK3wviOe9lvRgj/H3Pwt +bewb0l+MhRig0/DVHamyVxrDRbqInU1/GTNCwcZkXKYFWSf92U+kIcTth24Q1gcw +eZiLl5FfrWokUNytFElXob0V0a5/kbhiLc3yWmvWqHTpqCALbVyF+rKJo2f5Kw== +-----END CERTIFICATE-----
diff --git a/grpc/interop/lib/src/client.dart b/grpc/interop/lib/src/client.dart new file mode 100644 index 0000000..cb13569 --- /dev/null +++ b/grpc/interop/lib/src/client.dart
@@ -0,0 +1,1165 @@ +// Copyright (c) 2017, the gRPC project authors. Please see the AUTHORS file +// for details. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import 'dart:async'; +import 'dart:io'; +import 'dart:typed_data'; + +import 'package:collection/collection.dart'; +import 'package:grpc/grpc.dart'; +import 'generated/empty.pb.dart'; +import 'generated/messages.pb.dart'; +import 'generated/test.pbgrpc.dart'; + +const _headerEchoKey = 'x-grpc-test-echo-initial'; +const _headerEchoData = 'test_initial_metadata_value'; + +const _trailerEchoKey = 'x-grpc-test-echo-trailing-bin'; +const _trailerEchoData = 'q6ur'; // 0xababab in base64 + +class Tester { + final String serverHost; + final String? serverHostOverride; + final int serverPort; + final String testCase; + final bool useTls; + final bool useTestCA; + final String? defaultServiceAccount; + final String? oauthScope; + final String? serviceAccountKeyFile; + String? _serviceAccountJson; + + Tester( + {required this.serverHost, + required this.serverHostOverride, + required this.serverPort, + required this.testCase, + required this.useTls, + required this.useTestCA, + required this.defaultServiceAccount, + required this.oauthScope, + required this.serviceAccountKeyFile}); + + String get serviceAccountJson => + _serviceAccountJson ??= _readServiceAccountJson(); + + String _readServiceAccountJson() { + if (serviceAccountKeyFile?.isEmpty ?? true) { + throw 'Service account key file not specified.'; + } + return File(serviceAccountKeyFile!).readAsStringSync(); + } + + late final ClientChannel channel; + late final TestServiceClient client; + late final UnimplementedServiceClient unimplementedServiceClient; + + Future<void> runTest() async { + ChannelCredentials credentials; + if (useTls) { + List<int>? trustedRoot; + if (useTestCA) { + trustedRoot = File('ca.pem').readAsBytesSync(); + } + credentials = ChannelCredentials.secure( + certificates: trustedRoot, authority: serverHostOverride); + } else { + credentials = const ChannelCredentials.insecure(); + } + + final options = ChannelOptions(credentials: credentials); + channel = ClientChannel(serverHost, port: serverPort, options: options); + client = TestServiceClient(channel); + unimplementedServiceClient = UnimplementedServiceClient(channel); + await runTestCase(); + await channel.shutdown(); + } + + Future<void> runTestCase() async { + switch (testCase) { + case 'empty_unary': + return emptyUnary(); + case 'cacheable_unary': + return cacheableUnary(); + case 'large_unary': + return largeUnary(); + case 'client_compressed_unary': + return clientCompressedUnary(); + case 'server_compressed_unary': + return serverCompressedUnary(); + case 'client_streaming': + return clientStreaming(); + case 'client_compressed_streaming': + return clientCompressedStreaming(); + case 'server_streaming': + return serverStreaming(); + case 'server_compressed_streaming': + return serverCompressedStreaming(); + case 'ping_pong': + return pingPong(); + case 'empty_stream': + return emptyStream(); + case 'compute_engine_creds': + return computeEngineCreds(); + case 'service_account_creds': + return serviceAccountCreds(); + case 'jwt_token_creds': + return jwtTokenCreds(); + case 'oauth2_auth_token': + return oauth2AuthToken(); + case 'per_rpc_creds': + return perRpcCreds(); + case 'custom_metadata': + return customMetadata(); + case 'status_code_and_message': + return statusCodeAndMessage(); + case 'unimplemented_method': + return unimplementedMethod(); + case 'unimplemented_service': + return unimplementedService(); + case 'cancel_after_begin': + return cancelAfterBegin(); + case 'cancel_after_first_response': + return cancelAfterFirstResponse(); + case 'timeout_on_sleeping_server': + return timeoutOnSleepingServer(); + default: + print('Unknown test case: $testCase'); + } + } + + /// This test verifies that implementations support zero-size messages. + /// Ideally, client implementations would verify that the request and response + /// were zero bytes serialized, but this is generally prohibitive to perform, + /// so is not required. + /// + /// Procedure: + /// 1. Client calls EmptyCall with the default Empty message + /// + /// Client asserts: + /// * call was successful + /// * response is non-null + Future<void> emptyUnary() async { + final response = await client.emptyCall(Empty()); + if (response is! Empty) throw 'Expected Empty response.'; + } + + /// This test verifies that gRPC requests marked as cacheable use GET verb + /// instead of POST, and that server sets appropriate cache control headers + /// for the response to be cached by a proxy. This test requires that the + /// server is behind a caching proxy. Use of current timestamp in the request + /// prevents accidental cache matches left over from previous tests. + /// + /// Procedure: + /// 1. Client calls CacheableUnaryCall with `SimpleRequest` request with + /// payload set to current timestamp. Timestamp format is irrelevant, and + /// resolution is in nanoseconds. + /// Client adds a `x-user-ip` header with value `1.2.3.4` to the request. + /// This is done since some proxies such as GFE will not cache requests + /// from localhost. + /// Client marks the request as cacheable by setting the cacheable flag in + /// the request context. Longer term this should be driven by the method + /// option specified in the proto file itself. + /// 2. Client calls CacheableUnaryCall again immediately with the same request + /// and configuration as the previous call. + /// + /// Client asserts: + /// * Both calls were successful + /// * The payload body of both responses is the same. + Future<void> cacheableUnary() async { + throw 'Not implemented'; + } + + /// This test verifies unary calls succeed in sending messages, and touches on + /// flow control (even if compression is enabled on the channel). + /// + /// Procedure: + /// 1. Client calls UnaryCall with: + /// { + /// response_size: 314159 + /// payload: { + /// body: 271828 bytes of zeros + /// } + /// } + /// + /// Client asserts: + /// * call was successful + /// * response payload body is 314159 bytes in size + /// * clients are free to assert that the response payload body contents are + /// zero and comparing the entire response message against a golden response + Future<void> largeUnary() async { + final payload = Payload()..body = Uint8List(271828); + final request = SimpleRequest() + ..responseSize = 314159 + ..payload = payload; + final response = await client.unaryCall(request); + final receivedBytes = response.payload.body.length; + if (receivedBytes != 314159) { + throw 'Response payload mismatch. Expected 314159 bytes, ' + 'got ${receivedBytes}.'; + } + } + + /// This test verifies the client can compress unary messages by sending two + /// unary calls, for compressed and uncompressed payloads. It also sends an + /// initial probing request to verify whether the server supports the + /// CompressedRequest feature by checking if the probing call fails with an + /// `INVALID_ARGUMENT` status. + /// + /// Procedure: + /// 1. Client calls UnaryCall with the feature probe, an *uncompressed* + /// message: + /// { + /// expect_compressed: { + /// value: true + /// } + /// response_size: 314159 + /// payload: { + /// body: 271828 bytes of zeros + /// } + /// } + /// 1. Client calls UnaryCall with the *compressed* message: + /// { + /// expect_compressed: { + /// value: true + /// } + /// response_size: 314159 + /// payload: { + /// body: 271828 bytes of zeros + /// } + /// } + /// 1. Client calls UnaryCall with the *uncompressed* message: + /// { + /// expect_compressed: { + /// value: false + /// } + /// response_size: 314159 + /// payload: { + /// body: 271828 bytes of zeros + /// } + /// } + /// + /// Client asserts: + /// * First call failed with `INVALID_ARGUMENT` status. + /// * Subsequent calls were successful. + /// * Response payload body is 314159 bytes in size. + /// * Clients are free to assert that the response payload body contents are + /// zeros and comparing the entire response message against a golden + /// response. + Future<void> clientCompressedUnary() async { + throw 'Not implemented'; + } + + /// This test verifies the server can compress unary messages. It sends two + /// unary requests, expecting the server's response to be compressed or not + /// according to the `response_compressed` boolean. + /// + /// Whether compression was actually performed is determined by the + /// compression bit in the response's message flags. *Note that some languages + /// may not have access to the message flags, in which case the client will be + /// unable to verify that the `response_compressed` boolean is obeyed by the + /// server*. + /// + /// Procedure: + /// 1. Client calls UnaryCall with `SimpleRequest`: + /// { + /// response_compressed: { + /// value: true + /// } + /// response_size: 314159 + /// payload: { + /// body: 271828 bytes of zeros + /// } + /// } + /// { + /// response_compressed: { + /// value: false + /// } + /// response_size: 314159 + /// payload: { + /// body: 271828 bytes of zeros + /// } + /// } + /// + /// Client asserts: + /// * call was successful + /// * if supported by the implementation, when `response_compressed` is true, + /// the response MUST have the compressed message flag set. + /// * if supported by the implementation, when `response_compressed` is false, + /// the response MUST NOT have the compressed message flag set. + /// * response payload body is 314159 bytes in size in both cases. + /// * clients are free to assert that the response payload body contents are + /// zero and comparing the entire response message against a golden response + Future<void> serverCompressedUnary() async { + throw 'Not implemented'; + } + + /// This test verifies that client-only streaming succeeds. + /// + /// Procedure: + /// 1. Client calls StreamingInputCall + /// 2. Client sends: + /// { + /// payload: { + /// body: 27182 bytes of zeros + /// } + /// } + /// 3. Client then sends: + /// { + /// payload: { + /// body: 8 bytes of zeros + /// } + /// } + /// 4. Client then sends: + /// { + /// payload: { + /// body: 1828 bytes of zeros + /// } + /// } + /// 5. Client then sends: + /// { + /// payload: { + /// body: 45904 bytes of zeros + /// } + /// } + /// 6. Client half-closes + /// + /// Client asserts: + /// * call was successful + /// * response aggregated_payload_size is 74922 + Future<void> clientStreaming() async { + StreamingInputCallRequest createRequest(int bytes) { + final request = StreamingInputCallRequest()..payload = Payload(); + request.payload.body = Uint8List(bytes); + return request; + } + + Stream<StreamingInputCallRequest> requests() async* { + yield createRequest(27182); + yield createRequest(8); + yield createRequest(1828); + yield createRequest(45904); + } + + final response = await client.streamingInputCall(requests()); + if (response.aggregatedPayloadSize != 74922) { + throw 'Response mismatch. Expected 74922, ' + 'got ${response.aggregatedPayloadSize}'; + } + } + + /// This test verifies the client can compress requests on per-message basis + /// by performing a two-request streaming call. It also sends an initial + /// probing request to verify whether the server supports the + /// CompressedRequest feature by checking if the probing call fails with an + /// `INVALID_ARGUMENT` status. + /// + /// Procedure: + /// 1. Client calls `StreamingInputCall` and sends the following + /// feature-probing *uncompressed* `StreamingInputCallRequest` message + /// { + /// expect_compressed: { + /// value: true + /// } + /// payload: { + /// body: 27182 bytes of zeros + /// } + /// } + /// If the call does not fail with `INVALID_ARGUMENT`, the test fails. + /// Otherwise, we continue. + /// 1. Client calls `StreamingInputCall` again, sending the *compressed* + /// message + /// { + /// expect_compressed: { + /// value: true + /// } + /// payload: { + /// body: 27182 bytes of zeros + /// } + /// } + /// 1. And finally, the *uncompressed* message + /// { + /// expect_compressed: { + /// value: false + /// } + /// payload: { + /// body: 45904 bytes of zeros + /// } + /// } + /// 1. Client half-closes + /// + /// Client asserts: + /// * First call fails with `INVALID_ARGUMENT`. + /// * Next calls succeeds. + /// * Response aggregated payload size is 73086. + Future<void> clientCompressedStreaming() async { + throw 'Not implemented'; + } + + /// This test verifies that server-only streaming succeeds. + /// + /// Procedure: + /// 1. Client calls StreamingOutputCall with `StreamingOutputCallRequest`: + /// { + /// response_parameters: { + /// size: 31415 + /// } + /// response_parameters: { + /// size: 9 + /// } + /// response_parameters: { + /// size: 2653 + /// } + /// response_parameters: { + /// size: 58979 + /// } + /// } + /// + /// Client asserts: + /// * call was successful + /// * exactly four responses + /// * response payload bodies are sized (in order): 31415, 9, 2653, 58979 + /// * clients are free to assert that the response payload body contents are + /// zero and comparing the entire response messages against golden responses + Future<void> serverStreaming() async { + final expectedResponses = [31415, 9, 2653, 58979]; + + final request = StreamingOutputCallRequest() + ..responseParameters.addAll( + expectedResponses.map((size) => ResponseParameters()..size = size)); + + final responses = await client.streamingOutputCall(request).toList(); + if (responses.length != 4) { + throw 'Incorrect number of responses (${responses.length}).'; + } + final responseLengths = + responses.map((response) => response.payload.body.length).toList(); + + if (!ListEquality().equals(responseLengths, expectedResponses)) { + throw 'Incorrect response lengths received (${responseLengths.join(', ')} != ${expectedResponses.join(', ')})'; + } + } + + /// This test verifies that the server can compress streaming messages and + /// disable compression on individual messages, expecting the server's + /// response to be compressed or not according to the `response_compressed` + /// boolean. + /// + /// Whether compression was actually performed is determined by the + /// compression bit in the response's message flags. *Note that some languages + /// may not have access to the message flags, in which case the client will be + /// unable to verify that the `response_compressed` boolean is obeyed by the + /// server*. + /// + /// Procedure: + /// 1. Client calls StreamingOutputCall with `StreamingOutputCallRequest`: + /// { + /// response_parameters: { + /// compressed: { + /// value: true + /// } + /// size: 31415 + /// } + /// response_parameters: { + /// compressed: { + /// value: false + /// } + /// size: 92653 + /// } + /// } + /// + /// Client asserts: + /// * call was successful + /// * exactly two responses + /// * if supported by the implementation, when `response_compressed` is false, + /// the response's messages MUST NOT have the compressed message flag set. + /// * if supported by the implementation, when `response_compressed` is true, + /// the response's messages MUST have the compressed message flag set. + /// * response payload bodies are sized (in order): 31415, 92653 + /// * clients are free to assert that the response payload body contents are + /// zero and comparing the entire response messages against golden responses + Future<void> serverCompressedStreaming() async { + throw 'Not implemented'; + } + + /// This test verifies that full duplex bidi is supported. + /// + /// Procedure: + /// 1. Client calls FullDuplexCall with: + /// { + /// response_parameters: { + /// size: 31415 + /// } + /// payload: { + /// body: 27182 bytes of zeros + /// } + /// } + /// 2. After getting a reply, it sends: + /// { + /// response_parameters: { + /// size: 9 + /// } + /// payload: { + /// body: 8 bytes of zeros + /// } + /// } + /// 3. After getting a reply, it sends: + /// { + /// response_parameters: { + /// size: 2653 + /// } + /// payload: { + /// body: 1828 bytes of zeros + /// } + /// } + /// 4. After getting a reply, it sends: + /// { + /// response_parameters: { + /// size: 58979 + /// } + /// payload: { + /// body: 45904 bytes of zeros + /// } + /// } + /// 5. After getting a reply, client half-closes + /// + /// Client asserts: + /// * call was successful + /// * exactly four responses + /// * response payload bodies are sized (in order): 31415, 9, 2653, 58979 + /// * clients are free to assert that the response payload body contents are + /// zero and comparing the entire response messages against golden responses + Future<void> pingPong() async { + final requestSizes = [27182, 8, 1828, 45904]; + final expectedResponses = [31415, 9, 2653, 58979]; + + StreamingOutputCallRequest createRequest(int index) { + final payload = Payload()..body = Uint8List(requestSizes[index]); + final request = StreamingOutputCallRequest() + ..payload = payload + ..responseParameters + .add(ResponseParameters()..size = expectedResponses[index]); + return request; + } + + var index = 0; + final requests = StreamController<int>(); + + final responses = client.fullDuplexCall(requests.stream.map(createRequest)); + requests.add(index); + await for (final response in responses) { + if (index >= expectedResponses.length) { + throw 'Received too many responses. $index > ${expectedResponses.length}.'; + } + if (response.payload.body.length != expectedResponses[index]) { + throw 'Response mismatch for response $index: ' + '${response.payload.body.length} != ${expectedResponses[index]}.'; + } + index++; + if (index == requestSizes.length) { + requests.close(); + } else { + requests.add(index); + } + } + } + + /// This test verifies that streams support having zero-messages in both + /// directions. + /// + /// Procedure: + /// 1. Client calls FullDuplexCall and then half-closes + /// + /// Client asserts: + /// * call was successful + /// * exactly zero responses + Future<void> emptyStream() async { + final requests = StreamController<StreamingOutputCallRequest>(); + final call = client.fullDuplexCall(requests.stream); + requests.close(); + final responses = await call.toList(); + if (responses.isNotEmpty) { + throw 'Received too many responses. ${responses.length} != 0'; + } + } + + /// This test is only for cloud-to-prod path. + /// + /// This test verifies unary calls succeed in sending messages while using + /// Service Credentials from GCE metadata server. The client instance needs to + /// be created with desired oauth scope. + /// + /// The test uses `--default_service_account` with GCE service account email + /// and `--oauth_scope` with the OAuth scope to use. For testing against + /// grpc-test.sandbox.googleapis.com, + /// "https://www.googleapis.com/auth/xapi.zoo" should be passed in as + /// `--oauth_scope`. + /// + /// Procedure: + /// 1. Client configures channel to use GCECredentials + /// 2. Client calls UnaryCall on the channel with: + /// { + /// response_size: 314159 + /// payload: { + /// body: 271828 bytes of zeros + /// } + /// fill_username: true + /// fill_oauth_scope: true + /// } + /// + /// Client asserts: + /// * call was successful + /// * received SimpleResponse.username equals the value of + /// `--default_service_account` flag + /// * received SimpleResponse.oauth_scope is in `--oauth_scope` + /// * response payload body is 314159 bytes in size + /// * clients are free to assert that the response payload body contents are + /// zero and comparing the entire response message against a golden response + Future<void> computeEngineCreds() async { + final credentials = ComputeEngineAuthenticator(); + final clientWithCredentials = + TestServiceClient(channel, options: credentials.toCallOptions); + + final response = await _sendSimpleRequestForAuth(clientWithCredentials, + fillUsername: true, fillOauthScope: true); + + final user = response.username; + final oauth = response.oauthScope; + + if (user.isEmpty) { + throw 'Username not received.'; + } + if (oauth.isEmpty) { + throw 'OAuth scope not received.'; + } + + if (user != defaultServiceAccount) { + throw 'Got user name $user, wanted $defaultServiceAccount'; + } + if (!oauthScope!.contains(oauth)) { + throw 'Got OAuth scope $oauth, which is not a substring of $oauthScope'; + } + } + + /// This test is only for cloud-to-prod path. + /// + /// This test verifies unary calls succeed in sending messages while using + /// service account credentials. + /// + /// Test caller should set flag `--service_account_key_file` with the path to + /// json key file downloaded from https://console.developers.google.com. + /// Alternately, if using a usable auth implementation, she may specify the + /// file location in the environment variable GOOGLE_APPLICATION_CREDENTIALS. + /// + /// Procedure: + /// 1. Client configures the channel to use ServiceAccountCredentials + /// 2. Client calls UnaryCall with: + /// { + /// response_size: 314159 + /// payload: { + /// body: 271828 bytes of zeros + /// } + /// fill_username: true + /// } + /// + /// Client asserts: + /// * call was successful + /// * received SimpleResponse.username is not empty and is in the json key + /// file used by the auth library. The client can optionally check the + /// username matches the email address in the key file or equals the value + /// of `--default_service_account` flag. + /// * response payload body is 314159 bytes in size + /// * clients are free to assert that the response payload body contents are + /// zero and comparing the entire response message against a golden response + Future<void> serviceAccountCreds() async { + throw 'Not implemented'; + } + + /// This test is only for cloud-to-prod path. + /// + /// This test verifies unary calls succeed in sending messages while using JWT + /// token (created by the project's key file) + /// + /// Test caller should set flag `--service_account_key_file` with the path to + /// json key file downloaded from https://console.developers.google.com. + /// Alternately, if using a usable auth implementation, she may specify the + /// file location in the environment variable GOOGLE_APPLICATION_CREDENTIALS. + /// + /// Procedure: + /// 1. Client configures the channel to use JWTTokenCredentials + /// 2. Client calls UnaryCall with: + /// { + /// response_size: 314159 + /// payload: { + /// body: 271828 bytes of zeros + /// } + /// fill_username: true + /// } + /// + /// Client asserts: + /// * call was successful + /// * received SimpleResponse.username is not empty and is in the json key + /// file used by the auth library. The client can optionally check the + /// username matches the email address in the key file or equals the value + /// of `--default_service_account` flag. + /// * response payload body is 314159 bytes in size + /// * clients are free to assert that the response payload body contents are + /// zero and comparing the entire response message against a golden response + Future<void> jwtTokenCreds() async { + final credentials = JwtServiceAccountAuthenticator(serviceAccountJson); + final clientWithCredentials = + TestServiceClient(channel, options: credentials.toCallOptions); + + final response = await _sendSimpleRequestForAuth(clientWithCredentials, + fillUsername: true); + final username = response.username; + if (username.isEmpty) { + throw 'Username not received.'; + } + if (!serviceAccountJson.contains(username)) { + throw 'Got user name $username, which is not a substring of $serviceAccountJson'; + } + } + + /// This test is only for cloud-to-prod path and some implementations may run + /// in GCE only. + /// + /// This test verifies unary calls succeed in sending messages using an OAuth2 + /// token that is obtained out of band. For the purpose of the test, the + /// OAuth2 token is actually obtained from a service account credentials or + /// GCE credentials via the language-specific authorization library. + /// + /// The difference between this test and the other auth tests is that it first + /// uses the authorization library to obtain an authorization token. + /// + /// The test + /// * uses the flag `--service_account_key_file` with the path to a json key + /// file downloaded from https://console.developers.google.com. Alternately, + /// if using a usable auth implementation, it may specify the file location + /// in the environment variable GOOGLE_APPLICATION_CREDENTIALS, *OR* if GCE + /// credentials is used to fetch the token, `--default_service_account` can + /// be used to pass in GCE service account email. + /// * uses the flag `--oauth_scope` for the oauth scope. For testing against + /// grpc-test.sandbox.googleapis.com, + /// "https://www.googleapis.com/auth/xapi.zoo" should be passed as the + /// `--oauth_scope`. + /// + /// Procedure: + /// 1. Client uses the auth library to obtain an authorization token + /// 2. Client configures the channel to use AccessTokenCredentials with the + /// access token obtained in step 1 + /// 3. Client calls UnaryCall with the following message + /// { + /// fill_username: true + /// fill_oauth_scope: true + /// } + /// + /// Client asserts: + /// * call was successful + /// * received SimpleResponse.username is valid. Depending on whether a + /// service account key file or GCE credentials was used, client should + /// check against the json key file or GCE default service account email. + /// * received SimpleResponse.oauth_scope is in `--oauth_scope` + Future<void> oauth2AuthToken() async { + final credentials = + ServiceAccountAuthenticator(serviceAccountJson, [oauthScope!]); + final clientWithCredentials = + TestServiceClient(channel, options: credentials.toCallOptions); + + final response = await _sendSimpleRequestForAuth(clientWithCredentials, + fillUsername: true, fillOauthScope: true); + + final user = response.username; + final oauth = response.oauthScope; + + if (user.isEmpty) { + throw 'Username not received.'; + } + if (oauth.isEmpty) { + throw 'OAuth scope not received.'; + } + + if (!serviceAccountJson.contains(user)) { + throw 'Got user name $user, which is not' + ' a substring of $serviceAccountJson'; + } + if (!oauthScope!.contains(oauth)) { + throw 'Got OAuth scope $oauth, which is not a substring of $oauthScope'; + } + } + + /// Similar to the other auth tests, this test is only for cloud-to-prod path. + /// + /// This test verifies unary calls succeed in sending messages using a JWT or + /// a service account credentials set on the RPC. + /// + /// The test + /// * uses the flag `--service_account_key_file` with the path to a json key + /// file downloaded from https://console.developers.google.com. Alternately, + /// if using a usable auth implementation, it may specify the file location + /// in the environment variable GOOGLE_APPLICATION_CREDENTIALS + /// * optionally uses the flag `--oauth_scope` for the oauth scope if + /// implementor wishes to use service account credential instead of JWT + /// credential. For testing against grpc-test.sandbox.googleapis.com, oauth + /// scope "https://www.googleapis.com/auth/xapi.zoo" should be used. + /// + /// Procedure: + /// 1. Client configures the channel with just SSL credentials + /// 2. Client calls UnaryCall, setting per-call credentials to + /// JWTTokenCredentials. The request is the following message + /// { + /// fill_username: true + /// } + /// + /// Client asserts: + /// * call was successful + /// * received SimpleResponse.username is not empty and is in the json key + /// file used by the auth library. The client can optionally check the + /// username matches the email address in the key file. + Future<void> perRpcCreds() async { + final credentials = + ServiceAccountAuthenticator(serviceAccountJson, [oauthScope!]); + + final response = await _sendSimpleRequestForAuth(client, + fillUsername: true, + fillOauthScope: true, + options: credentials.toCallOptions); + + final user = response.username; + final oauth = response.oauthScope; + + if (user.isEmpty) { + throw 'Username not received.'; + } + if (oauth.isEmpty) { + throw 'OAuth scope not received.'; + } + + if (!serviceAccountJson.contains(user)) { + throw 'Got user name $user, which is not a substring of $serviceAccountJson'; + } + if (!oauthScope!.contains(oauth)) { + throw 'Got OAuth scope $oauth, which is not a substring of $oauthScope'; + } + } + + Future<SimpleResponse> _sendSimpleRequestForAuth(TestServiceClient client, + {bool fillUsername = false, + bool fillOauthScope = false, + CallOptions? options}) async { + final payload = Payload()..body = Uint8List(271828); + final request = SimpleRequest() + ..responseSize = 314159 + ..payload = payload + ..fillUsername = fillUsername + ..fillOauthScope = fillOauthScope; + final response = await client.unaryCall(request, options: options); + final receivedBytes = response.payload.body.length; + if (receivedBytes != 314159) { + throw 'Response payload mismatch. Expected 314159 bytes, ' + 'got ${receivedBytes}.'; + } + return response; + } + + /// This test verifies that custom metadata in either binary or ascii format + /// can be sent as initial-metadata by the client and as both initial- and + /// trailing-metadata by the server. + /// + /// Procedure: + /// 1. The client attaches custom metadata with the following keys and values: + /// key: "x-grpc-test-echo-initial", value: "test_initial_metadata_value" + /// key: "x-grpc-test-echo-trailing-bin", value: 0xababab + /// to a UnaryCall with request: + /// { + /// response_size: 314159 + /// payload: { + /// body: 271828 bytes of zeros + /// } + /// } + /// + /// 2. The client attaches custom metadata with the following keys and values: + /// key: "x-grpc-test-echo-initial", value: "test_initial_metadata_value" + /// key: "x-grpc-test-echo-trailing-bin", value: 0xababab + /// to a FullDuplexCall with request: + /// { + /// response_parameters: { + /// size: 314159 + /// } + /// payload: { + /// body: 271828 bytes of zeros + /// } + /// } + /// and then half-closes + /// + /// Client asserts: + /// * call was successful + /// * metadata with key `"x-grpc-test-echo-initial"` and value + /// `"test_initial_metadata_value"`is received in the initial metadata for + /// calls in Procedure steps 1 and 2. + /// * metadata with key `"x-grpc-test-echo-trailing-bin"` and value `0xababab` + /// is received in the trailing metadata for calls in Procedure steps 1 and + /// 2. + Future<void> customMetadata() async { + void validate(Map<String, String> headers, Map<String, String> trailers) { + if (headers[_headerEchoKey] != _headerEchoData) { + throw 'Invalid header data received.'; + } + if (trailers[_trailerEchoKey] != _trailerEchoData) { + throw 'Invalid trailer data received.'; + } + } + + final options = CallOptions(metadata: { + _headerEchoKey: _headerEchoData, + _trailerEchoKey: _trailerEchoData, + }); + final unaryCall = client.unaryCall( + SimpleRequest() + ..responseSize = 314159 + ..payload = (Payload()..body = Uint8List(271828)), + options: options); + var headers = await unaryCall.headers; + var trailers = await unaryCall.trailers; + await unaryCall; + validate(headers, trailers); + + Stream<StreamingOutputCallRequest> requests() async* { + yield StreamingOutputCallRequest() + ..responseParameters.add(ResponseParameters()..size = 314159) + ..payload = (Payload()..body = Uint8List(271828)); + } + + final fullDuplexCall = client.fullDuplexCall(requests(), options: options); + final drain = fullDuplexCall.drain(); + headers = await fullDuplexCall.headers; + trailers = await fullDuplexCall.trailers; + await drain; + validate(headers, trailers); + } + + /// This test verifies unary calls succeed in sending messages, and propagate + /// back status code and message sent along with the messages. + /// + /// Procedure: + /// 1. Client calls UnaryCall with: + /// { + /// response_status: { + /// code: 2 + /// message: "test status message" + /// } + /// } + /// + /// 2. Client calls FullDuplexCall with: + /// { + /// response_status: { + /// code: 2 + /// message: "test status message" + /// } + /// } + /// + /// and then half-closes + /// + /// Client asserts: + /// * received status code is the same as the sent code for both Procedure + /// steps 1 and 2 + /// * received status message is the same as the sent message for both + /// Procedure steps 1 and 2 + Future<void> statusCodeAndMessage() async { + final expectedStatus = GrpcError.custom(2, 'test status message'); + final responseStatus = EchoStatus() + ..code = expectedStatus.code + ..message = expectedStatus.message!; + try { + await client.unaryCall(SimpleRequest()..responseStatus = responseStatus); + throw 'Did not receive correct status code.'; + } on GrpcError catch (e) { + if (e != expectedStatus) { + throw 'Received incorrect status: $e.'; + } + } + Stream<StreamingOutputCallRequest> requests() async* { + yield StreamingOutputCallRequest()..responseStatus = responseStatus; + } + + try { + await for (final _ in client.fullDuplexCall(requests())) { + throw 'Received unexpected response.'; + } + throw 'Did not receive correct status code.'; + } on GrpcError catch (e) { + if (e != expectedStatus) { + throw 'Received incorrect status: $e.'; + } + } + } + + /// This test verifies that calling an unimplemented RPC method returns the + /// UNIMPLEMENTED status code. + /// + /// Procedure: + /// * Client calls `grpc.testing.TestService/UnimplementedCall` with an empty + /// request (defined as `grpc.testing.Empty`): + /// { + /// } + /// + /// Client asserts: + /// * received status code is 12 (UNIMPLEMENTED) + Future<void> unimplementedMethod() async { + try { + await client.unimplementedCall(Empty()); + throw 'Did not throw.'; + } on GrpcError catch (e) { + if (e.code != StatusCode.unimplemented) { + throw 'Unexpected status code ${e.code} - ${e.message}.'; + } + } + } + + /// This test verifies calling an unimplemented server returns the + /// UNIMPLEMENTED status code. + /// + /// Procedure: + /// * Client calls `grpc.testing.UnimplementedService/UnimplementedCall` with + /// an empty request (defined as `grpc.testing.Empty`) + /// + /// Client asserts: + /// * received status code is 12 (UNIMPLEMENTED) + Future<void> unimplementedService() async { + try { + await unimplementedServiceClient.unimplementedCall(Empty()); + throw 'Did not throw.'; + } on GrpcError catch (e) { + if (e.code != StatusCode.unimplemented) { + throw 'Unexpected status code ${e.code} - ${e.message}.'; + } + } + } + + /// This test verifies that a request can be cancelled after metadata has been + /// sent but before payloads are sent. + /// + /// Procedure: + /// 1. Client starts StreamingInputCall + /// 2. Client immediately cancels request + /// + /// Client asserts: + /// * Call completed with status CANCELLED + Future<void> cancelAfterBegin() async { + final requests = StreamController<StreamingInputCallRequest>(); + final call = client.streamingInputCall(requests.stream); + scheduleMicrotask(call.cancel); + try { + await call; + throw 'Expected exception.'; + } on GrpcError catch (e) { + if (e.code != StatusCode.cancelled) { + throw 'Unexpected status code ${e.code} - ${e.message}'; + } + } + requests.close(); + } + + /// This test verifies that a request can be cancelled after receiving a + /// message from the server. + /// + /// Procedure: + /// 1. Client starts FullDuplexCall with + /// { + /// response_parameters: { + /// size: 31415 + /// } + /// payload: { + /// body: 27182 bytes of zeros + /// } + /// } + /// + /// 2. After receiving a response, client cancels request + /// + /// Client asserts: + /// * Call completed with status CANCELLED + Future<void> cancelAfterFirstResponse() async { + final requests = StreamController<StreamingOutputCallRequest>(); + final call = client.fullDuplexCall(requests.stream); + final completer = Completer(); + + var receivedResponse = false; + call.listen((response) { + if (receivedResponse) { + completer.completeError('Received too many responses.'); + return; + } + receivedResponse = true; + if (response.payload.body.length != 31415) { + completer.completeError('Invalid response length: ' + '${response.payload.body.length} != 31415.'); + } + call.cancel(); + }, onError: (e) { + if (e is! GrpcError) { + completer.completeError('Unexpected error: $e.'); + } else if (e.code != StatusCode.cancelled) { + completer + .completeError('Unexpected status code ${e.code}: ${e.message}.'); + } else { + completer.complete(true); + } + }, onDone: () { + if (!completer.isCompleted) completer.completeError('Expected error.'); + }); + + requests.add(StreamingOutputCallRequest() + ..responseParameters.add(ResponseParameters()..size = 31415) + ..payload = (Payload()..body = Uint8List(27182))); + await completer.future; + requests.close(); + } + + /// This test verifies that an RPC request whose lifetime exceeds its + /// configured timeout value will end with the DeadlineExceeded status. + /// + /// Procedure: + /// 1. Client calls FullDuplexCall with the following request and sets its + /// timeout to 1ms + /// { + /// payload: { + /// body: 27182 bytes of zeros + /// } + /// } + /// + /// 2. Client waits + /// + /// Client asserts: + /// * Call completed with status DEADLINE_EXCEEDED. + Future<void> timeoutOnSleepingServer() async { + final requests = StreamController<StreamingOutputCallRequest>(); + final call = client.fullDuplexCall(requests.stream, + options: CallOptions(timeout: Duration(milliseconds: 1))); + requests.add(StreamingOutputCallRequest() + ..payload = (Payload()..body = Uint8List(27182))); + try { + await for (final _ in call) { + throw 'Unexpected response received.'; + } + throw 'Expected exception.'; + } on GrpcError catch (e) { + if (e.code != StatusCode.deadlineExceeded) { + throw 'Unexpected status code ${e.code} - ${e.message}.'; + } + } finally { + requests.close(); + } + } +}
diff --git a/grpc/interop/lib/src/generated/empty.pb.dart b/grpc/interop/lib/src/generated/empty.pb.dart new file mode 100644 index 0000000..5d81eb8 --- /dev/null +++ b/grpc/interop/lib/src/generated/empty.pb.dart
@@ -0,0 +1,51 @@ +/// +// Generated code. Do not modify. +// source: empty.proto +// +// @dart = 2.12 +// ignore_for_file: annotate_overrides,camel_case_types,unnecessary_const,non_constant_identifier_names,library_prefixes,unused_import,unused_shown_name,return_of_invalid_type,unnecessary_this,prefer_final_fields + +import 'dart:core' as $core; + +import 'package:protobuf/protobuf.dart' as $pb; + +class Empty extends $pb.GeneratedMessage { + static final $pb.BuilderInfo _i = $pb.BuilderInfo( + const $core.bool.fromEnvironment('protobuf.omit_message_names') + ? '' + : 'Empty', + package: const $pb.PackageName( + const $core.bool.fromEnvironment('protobuf.omit_message_names') + ? '' + : 'grpc.testing'), + createEmptyInstance: create) + ..hasRequiredFields = false; + + Empty._() : super(); + factory Empty() => create(); + factory Empty.fromBuffer($core.List<$core.int> i, + [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromBuffer(i, r); + factory Empty.fromJson($core.String i, + [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromJson(i, r); + @$core.Deprecated('Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.deepCopy] instead. ' + 'Will be removed in next major version') + Empty clone() => Empty()..mergeFromMessage(this); + @$core.Deprecated('Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.rebuild] instead. ' + 'Will be removed in next major version') + Empty copyWith(void Function(Empty) updates) => + super.copyWith((message) => updates(message as Empty)) + as Empty; // ignore: deprecated_member_use + $pb.BuilderInfo get info_ => _i; + @$core.pragma('dart2js:noInline') + static Empty create() => Empty._(); + Empty createEmptyInstance() => create(); + static $pb.PbList<Empty> createRepeated() => $pb.PbList<Empty>(); + @$core.pragma('dart2js:noInline') + static Empty getDefault() => + _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor<Empty>(create); + static Empty? _defaultInstance; +}
diff --git a/grpc/interop/lib/src/generated/messages.pb.dart b/grpc/interop/lib/src/generated/messages.pb.dart new file mode 100644 index 0000000..b659a1c --- /dev/null +++ b/grpc/interop/lib/src/generated/messages.pb.dart
@@ -0,0 +1,1242 @@ +/// +// Generated code. Do not modify. +// source: messages.proto +// +// @dart = 2.12 +// ignore_for_file: annotate_overrides,camel_case_types,unnecessary_const,non_constant_identifier_names,library_prefixes,unused_import,unused_shown_name,return_of_invalid_type,unnecessary_this,prefer_final_fields + +import 'dart:core' as $core; + +import 'package:protobuf/protobuf.dart' as $pb; + +import 'messages.pbenum.dart'; + +export 'messages.pbenum.dart'; + +class BoolValue extends $pb.GeneratedMessage { + static final $pb.BuilderInfo _i = $pb.BuilderInfo( + const $core.bool.fromEnvironment('protobuf.omit_message_names') + ? '' + : 'BoolValue', + package: const $pb.PackageName( + const $core.bool.fromEnvironment('protobuf.omit_message_names') + ? '' + : 'grpc.testing'), + createEmptyInstance: create) + ..aOB( + 1, + const $core.bool.fromEnvironment('protobuf.omit_field_names') + ? '' + : 'value') + ..hasRequiredFields = false; + + BoolValue._() : super(); + factory BoolValue({ + $core.bool? value, + }) { + final _result = create(); + if (value != null) { + _result.value = value; + } + return _result; + } + factory BoolValue.fromBuffer($core.List<$core.int> i, + [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromBuffer(i, r); + factory BoolValue.fromJson($core.String i, + [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromJson(i, r); + @$core.Deprecated('Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.deepCopy] instead. ' + 'Will be removed in next major version') + BoolValue clone() => BoolValue()..mergeFromMessage(this); + @$core.Deprecated('Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.rebuild] instead. ' + 'Will be removed in next major version') + BoolValue copyWith(void Function(BoolValue) updates) => + super.copyWith((message) => updates(message as BoolValue)) + as BoolValue; // ignore: deprecated_member_use + $pb.BuilderInfo get info_ => _i; + @$core.pragma('dart2js:noInline') + static BoolValue create() => BoolValue._(); + BoolValue createEmptyInstance() => create(); + static $pb.PbList<BoolValue> createRepeated() => $pb.PbList<BoolValue>(); + @$core.pragma('dart2js:noInline') + static BoolValue getDefault() => + _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor<BoolValue>(create); + static BoolValue? _defaultInstance; + + @$pb.TagNumber(1) + $core.bool get value => $_getBF(0); + @$pb.TagNumber(1) + set value($core.bool v) { + $_setBool(0, v); + } + + @$pb.TagNumber(1) + $core.bool hasValue() => $_has(0); + @$pb.TagNumber(1) + void clearValue() => clearField(1); +} + +class Payload extends $pb.GeneratedMessage { + static final $pb.BuilderInfo _i = $pb.BuilderInfo( + const $core.bool.fromEnvironment('protobuf.omit_message_names') + ? '' + : 'Payload', + package: const $pb.PackageName( + const $core.bool.fromEnvironment('protobuf.omit_message_names') + ? '' + : 'grpc.testing'), + createEmptyInstance: create) + ..e<PayloadType>( + 1, + const $core.bool.fromEnvironment('protobuf.omit_field_names') + ? '' + : 'type', + $pb.PbFieldType.OE, + defaultOrMaker: PayloadType.COMPRESSABLE, + valueOf: PayloadType.valueOf, + enumValues: PayloadType.values) + ..a<$core.List<$core.int>>( + 2, + const $core.bool.fromEnvironment('protobuf.omit_field_names') + ? '' + : 'body', + $pb.PbFieldType.OY) + ..hasRequiredFields = false; + + Payload._() : super(); + factory Payload({ + PayloadType? type, + $core.List<$core.int>? body, + }) { + final _result = create(); + if (type != null) { + _result.type = type; + } + if (body != null) { + _result.body = body; + } + return _result; + } + factory Payload.fromBuffer($core.List<$core.int> i, + [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromBuffer(i, r); + factory Payload.fromJson($core.String i, + [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromJson(i, r); + @$core.Deprecated('Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.deepCopy] instead. ' + 'Will be removed in next major version') + Payload clone() => Payload()..mergeFromMessage(this); + @$core.Deprecated('Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.rebuild] instead. ' + 'Will be removed in next major version') + Payload copyWith(void Function(Payload) updates) => + super.copyWith((message) => updates(message as Payload)) + as Payload; // ignore: deprecated_member_use + $pb.BuilderInfo get info_ => _i; + @$core.pragma('dart2js:noInline') + static Payload create() => Payload._(); + Payload createEmptyInstance() => create(); + static $pb.PbList<Payload> createRepeated() => $pb.PbList<Payload>(); + @$core.pragma('dart2js:noInline') + static Payload getDefault() => + _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor<Payload>(create); + static Payload? _defaultInstance; + + @$pb.TagNumber(1) + PayloadType get type => $_getN(0); + @$pb.TagNumber(1) + set type(PayloadType v) { + setField(1, v); + } + + @$pb.TagNumber(1) + $core.bool hasType() => $_has(0); + @$pb.TagNumber(1) + void clearType() => clearField(1); + + @$pb.TagNumber(2) + $core.List<$core.int> get body => $_getN(1); + @$pb.TagNumber(2) + set body($core.List<$core.int> v) { + $_setBytes(1, v); + } + + @$pb.TagNumber(2) + $core.bool hasBody() => $_has(1); + @$pb.TagNumber(2) + void clearBody() => clearField(2); +} + +class EchoStatus extends $pb.GeneratedMessage { + static final $pb.BuilderInfo _i = $pb.BuilderInfo( + const $core.bool.fromEnvironment('protobuf.omit_message_names') + ? '' + : 'EchoStatus', + package: const $pb.PackageName( + const $core.bool.fromEnvironment('protobuf.omit_message_names') + ? '' + : 'grpc.testing'), + createEmptyInstance: create) + ..a<$core.int>( + 1, + const $core.bool.fromEnvironment('protobuf.omit_field_names') + ? '' + : 'code', + $pb.PbFieldType.O3) + ..aOS( + 2, + const $core.bool.fromEnvironment('protobuf.omit_field_names') + ? '' + : 'message') + ..hasRequiredFields = false; + + EchoStatus._() : super(); + factory EchoStatus({ + $core.int? code, + $core.String? message, + }) { + final _result = create(); + if (code != null) { + _result.code = code; + } + if (message != null) { + _result.message = message; + } + return _result; + } + factory EchoStatus.fromBuffer($core.List<$core.int> i, + [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromBuffer(i, r); + factory EchoStatus.fromJson($core.String i, + [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromJson(i, r); + @$core.Deprecated('Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.deepCopy] instead. ' + 'Will be removed in next major version') + EchoStatus clone() => EchoStatus()..mergeFromMessage(this); + @$core.Deprecated('Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.rebuild] instead. ' + 'Will be removed in next major version') + EchoStatus copyWith(void Function(EchoStatus) updates) => + super.copyWith((message) => updates(message as EchoStatus)) + as EchoStatus; // ignore: deprecated_member_use + $pb.BuilderInfo get info_ => _i; + @$core.pragma('dart2js:noInline') + static EchoStatus create() => EchoStatus._(); + EchoStatus createEmptyInstance() => create(); + static $pb.PbList<EchoStatus> createRepeated() => $pb.PbList<EchoStatus>(); + @$core.pragma('dart2js:noInline') + static EchoStatus getDefault() => _defaultInstance ??= + $pb.GeneratedMessage.$_defaultFor<EchoStatus>(create); + static EchoStatus? _defaultInstance; + + @$pb.TagNumber(1) + $core.int get code => $_getIZ(0); + @$pb.TagNumber(1) + set code($core.int v) { + $_setSignedInt32(0, v); + } + + @$pb.TagNumber(1) + $core.bool hasCode() => $_has(0); + @$pb.TagNumber(1) + void clearCode() => clearField(1); + + @$pb.TagNumber(2) + $core.String get message => $_getSZ(1); + @$pb.TagNumber(2) + set message($core.String v) { + $_setString(1, v); + } + + @$pb.TagNumber(2) + $core.bool hasMessage() => $_has(1); + @$pb.TagNumber(2) + void clearMessage() => clearField(2); +} + +class SimpleRequest extends $pb.GeneratedMessage { + static final $pb.BuilderInfo _i = $pb.BuilderInfo( + const $core.bool.fromEnvironment('protobuf.omit_message_names') + ? '' + : 'SimpleRequest', + package: const $pb.PackageName( + const $core.bool.fromEnvironment('protobuf.omit_message_names') + ? '' + : 'grpc.testing'), + createEmptyInstance: create) + ..e<PayloadType>( + 1, + const $core.bool.fromEnvironment('protobuf.omit_field_names') + ? '' + : 'responseType', + $pb.PbFieldType.OE, + defaultOrMaker: PayloadType.COMPRESSABLE, + valueOf: PayloadType.valueOf, + enumValues: PayloadType.values) + ..a<$core.int>( + 2, + const $core.bool.fromEnvironment('protobuf.omit_field_names') + ? '' + : 'responseSize', + $pb.PbFieldType.O3) + ..aOM<Payload>( + 3, + const $core.bool.fromEnvironment('protobuf.omit_field_names') + ? '' + : 'payload', + subBuilder: Payload.create) + ..aOB( + 4, + const $core.bool.fromEnvironment('protobuf.omit_field_names') + ? '' + : 'fillUsername') + ..aOB( + 5, + const $core.bool.fromEnvironment('protobuf.omit_field_names') + ? '' + : 'fillOauthScope') + ..aOM<BoolValue>( + 6, + const $core.bool.fromEnvironment('protobuf.omit_field_names') + ? '' + : 'responseCompressed', + subBuilder: BoolValue.create) + ..aOM<EchoStatus>( + 7, + const $core.bool.fromEnvironment('protobuf.omit_field_names') + ? '' + : 'responseStatus', + subBuilder: EchoStatus.create) + ..aOM<BoolValue>( + 8, + const $core.bool.fromEnvironment('protobuf.omit_field_names') + ? '' + : 'expectCompressed', + subBuilder: BoolValue.create) + ..hasRequiredFields = false; + + SimpleRequest._() : super(); + factory SimpleRequest({ + PayloadType? responseType, + $core.int? responseSize, + Payload? payload, + $core.bool? fillUsername, + $core.bool? fillOauthScope, + BoolValue? responseCompressed, + EchoStatus? responseStatus, + BoolValue? expectCompressed, + }) { + final _result = create(); + if (responseType != null) { + _result.responseType = responseType; + } + if (responseSize != null) { + _result.responseSize = responseSize; + } + if (payload != null) { + _result.payload = payload; + } + if (fillUsername != null) { + _result.fillUsername = fillUsername; + } + if (fillOauthScope != null) { + _result.fillOauthScope = fillOauthScope; + } + if (responseCompressed != null) { + _result.responseCompressed = responseCompressed; + } + if (responseStatus != null) { + _result.responseStatus = responseStatus; + } + if (expectCompressed != null) { + _result.expectCompressed = expectCompressed; + } + return _result; + } + factory SimpleRequest.fromBuffer($core.List<$core.int> i, + [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromBuffer(i, r); + factory SimpleRequest.fromJson($core.String i, + [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromJson(i, r); + @$core.Deprecated('Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.deepCopy] instead. ' + 'Will be removed in next major version') + SimpleRequest clone() => SimpleRequest()..mergeFromMessage(this); + @$core.Deprecated('Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.rebuild] instead. ' + 'Will be removed in next major version') + SimpleRequest copyWith(void Function(SimpleRequest) updates) => + super.copyWith((message) => updates(message as SimpleRequest)) + as SimpleRequest; // ignore: deprecated_member_use + $pb.BuilderInfo get info_ => _i; + @$core.pragma('dart2js:noInline') + static SimpleRequest create() => SimpleRequest._(); + SimpleRequest createEmptyInstance() => create(); + static $pb.PbList<SimpleRequest> createRepeated() => + $pb.PbList<SimpleRequest>(); + @$core.pragma('dart2js:noInline') + static SimpleRequest getDefault() => _defaultInstance ??= + $pb.GeneratedMessage.$_defaultFor<SimpleRequest>(create); + static SimpleRequest? _defaultInstance; + + @$pb.TagNumber(1) + PayloadType get responseType => $_getN(0); + @$pb.TagNumber(1) + set responseType(PayloadType v) { + setField(1, v); + } + + @$pb.TagNumber(1) + $core.bool hasResponseType() => $_has(0); + @$pb.TagNumber(1) + void clearResponseType() => clearField(1); + + @$pb.TagNumber(2) + $core.int get responseSize => $_getIZ(1); + @$pb.TagNumber(2) + set responseSize($core.int v) { + $_setSignedInt32(1, v); + } + + @$pb.TagNumber(2) + $core.bool hasResponseSize() => $_has(1); + @$pb.TagNumber(2) + void clearResponseSize() => clearField(2); + + @$pb.TagNumber(3) + Payload get payload => $_getN(2); + @$pb.TagNumber(3) + set payload(Payload v) { + setField(3, v); + } + + @$pb.TagNumber(3) + $core.bool hasPayload() => $_has(2); + @$pb.TagNumber(3) + void clearPayload() => clearField(3); + @$pb.TagNumber(3) + Payload ensurePayload() => $_ensure(2); + + @$pb.TagNumber(4) + $core.bool get fillUsername => $_getBF(3); + @$pb.TagNumber(4) + set fillUsername($core.bool v) { + $_setBool(3, v); + } + + @$pb.TagNumber(4) + $core.bool hasFillUsername() => $_has(3); + @$pb.TagNumber(4) + void clearFillUsername() => clearField(4); + + @$pb.TagNumber(5) + $core.bool get fillOauthScope => $_getBF(4); + @$pb.TagNumber(5) + set fillOauthScope($core.bool v) { + $_setBool(4, v); + } + + @$pb.TagNumber(5) + $core.bool hasFillOauthScope() => $_has(4); + @$pb.TagNumber(5) + void clearFillOauthScope() => clearField(5); + + @$pb.TagNumber(6) + BoolValue get responseCompressed => $_getN(5); + @$pb.TagNumber(6) + set responseCompressed(BoolValue v) { + setField(6, v); + } + + @$pb.TagNumber(6) + $core.bool hasResponseCompressed() => $_has(5); + @$pb.TagNumber(6) + void clearResponseCompressed() => clearField(6); + @$pb.TagNumber(6) + BoolValue ensureResponseCompressed() => $_ensure(5); + + @$pb.TagNumber(7) + EchoStatus get responseStatus => $_getN(6); + @$pb.TagNumber(7) + set responseStatus(EchoStatus v) { + setField(7, v); + } + + @$pb.TagNumber(7) + $core.bool hasResponseStatus() => $_has(6); + @$pb.TagNumber(7) + void clearResponseStatus() => clearField(7); + @$pb.TagNumber(7) + EchoStatus ensureResponseStatus() => $_ensure(6); + + @$pb.TagNumber(8) + BoolValue get expectCompressed => $_getN(7); + @$pb.TagNumber(8) + set expectCompressed(BoolValue v) { + setField(8, v); + } + + @$pb.TagNumber(8) + $core.bool hasExpectCompressed() => $_has(7); + @$pb.TagNumber(8) + void clearExpectCompressed() => clearField(8); + @$pb.TagNumber(8) + BoolValue ensureExpectCompressed() => $_ensure(7); +} + +class SimpleResponse extends $pb.GeneratedMessage { + static final $pb.BuilderInfo _i = $pb.BuilderInfo( + const $core.bool.fromEnvironment('protobuf.omit_message_names') + ? '' + : 'SimpleResponse', + package: const $pb.PackageName( + const $core.bool.fromEnvironment('protobuf.omit_message_names') + ? '' + : 'grpc.testing'), + createEmptyInstance: create) + ..aOM<Payload>( + 1, + const $core.bool.fromEnvironment('protobuf.omit_field_names') + ? '' + : 'payload', + subBuilder: Payload.create) + ..aOS( + 2, + const $core.bool.fromEnvironment('protobuf.omit_field_names') + ? '' + : 'username') + ..aOS( + 3, + const $core.bool.fromEnvironment('protobuf.omit_field_names') + ? '' + : 'oauthScope') + ..hasRequiredFields = false; + + SimpleResponse._() : super(); + factory SimpleResponse({ + Payload? payload, + $core.String? username, + $core.String? oauthScope, + }) { + final _result = create(); + if (payload != null) { + _result.payload = payload; + } + if (username != null) { + _result.username = username; + } + if (oauthScope != null) { + _result.oauthScope = oauthScope; + } + return _result; + } + factory SimpleResponse.fromBuffer($core.List<$core.int> i, + [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromBuffer(i, r); + factory SimpleResponse.fromJson($core.String i, + [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromJson(i, r); + @$core.Deprecated('Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.deepCopy] instead. ' + 'Will be removed in next major version') + SimpleResponse clone() => SimpleResponse()..mergeFromMessage(this); + @$core.Deprecated('Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.rebuild] instead. ' + 'Will be removed in next major version') + SimpleResponse copyWith(void Function(SimpleResponse) updates) => + super.copyWith((message) => updates(message as SimpleResponse)) + as SimpleResponse; // ignore: deprecated_member_use + $pb.BuilderInfo get info_ => _i; + @$core.pragma('dart2js:noInline') + static SimpleResponse create() => SimpleResponse._(); + SimpleResponse createEmptyInstance() => create(); + static $pb.PbList<SimpleResponse> createRepeated() => + $pb.PbList<SimpleResponse>(); + @$core.pragma('dart2js:noInline') + static SimpleResponse getDefault() => _defaultInstance ??= + $pb.GeneratedMessage.$_defaultFor<SimpleResponse>(create); + static SimpleResponse? _defaultInstance; + + @$pb.TagNumber(1) + Payload get payload => $_getN(0); + @$pb.TagNumber(1) + set payload(Payload v) { + setField(1, v); + } + + @$pb.TagNumber(1) + $core.bool hasPayload() => $_has(0); + @$pb.TagNumber(1) + void clearPayload() => clearField(1); + @$pb.TagNumber(1) + Payload ensurePayload() => $_ensure(0); + + @$pb.TagNumber(2) + $core.String get username => $_getSZ(1); + @$pb.TagNumber(2) + set username($core.String v) { + $_setString(1, v); + } + + @$pb.TagNumber(2) + $core.bool hasUsername() => $_has(1); + @$pb.TagNumber(2) + void clearUsername() => clearField(2); + + @$pb.TagNumber(3) + $core.String get oauthScope => $_getSZ(2); + @$pb.TagNumber(3) + set oauthScope($core.String v) { + $_setString(2, v); + } + + @$pb.TagNumber(3) + $core.bool hasOauthScope() => $_has(2); + @$pb.TagNumber(3) + void clearOauthScope() => clearField(3); +} + +class StreamingInputCallRequest extends $pb.GeneratedMessage { + static final $pb.BuilderInfo _i = $pb.BuilderInfo( + const $core.bool.fromEnvironment('protobuf.omit_message_names') + ? '' + : 'StreamingInputCallRequest', + package: const $pb.PackageName( + const $core.bool.fromEnvironment('protobuf.omit_message_names') + ? '' + : 'grpc.testing'), + createEmptyInstance: create) + ..aOM<Payload>( + 1, + const $core.bool.fromEnvironment('protobuf.omit_field_names') + ? '' + : 'payload', + subBuilder: Payload.create) + ..aOM<BoolValue>( + 2, + const $core.bool.fromEnvironment('protobuf.omit_field_names') + ? '' + : 'expectCompressed', + subBuilder: BoolValue.create) + ..hasRequiredFields = false; + + StreamingInputCallRequest._() : super(); + factory StreamingInputCallRequest({ + Payload? payload, + BoolValue? expectCompressed, + }) { + final _result = create(); + if (payload != null) { + _result.payload = payload; + } + if (expectCompressed != null) { + _result.expectCompressed = expectCompressed; + } + return _result; + } + factory StreamingInputCallRequest.fromBuffer($core.List<$core.int> i, + [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromBuffer(i, r); + factory StreamingInputCallRequest.fromJson($core.String i, + [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromJson(i, r); + @$core.Deprecated('Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.deepCopy] instead. ' + 'Will be removed in next major version') + StreamingInputCallRequest clone() => + StreamingInputCallRequest()..mergeFromMessage(this); + @$core.Deprecated('Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.rebuild] instead. ' + 'Will be removed in next major version') + StreamingInputCallRequest copyWith( + void Function(StreamingInputCallRequest) updates) => + super.copyWith((message) => updates(message as StreamingInputCallRequest)) + as StreamingInputCallRequest; // ignore: deprecated_member_use + $pb.BuilderInfo get info_ => _i; + @$core.pragma('dart2js:noInline') + static StreamingInputCallRequest create() => StreamingInputCallRequest._(); + StreamingInputCallRequest createEmptyInstance() => create(); + static $pb.PbList<StreamingInputCallRequest> createRepeated() => + $pb.PbList<StreamingInputCallRequest>(); + @$core.pragma('dart2js:noInline') + static StreamingInputCallRequest getDefault() => _defaultInstance ??= + $pb.GeneratedMessage.$_defaultFor<StreamingInputCallRequest>(create); + static StreamingInputCallRequest? _defaultInstance; + + @$pb.TagNumber(1) + Payload get payload => $_getN(0); + @$pb.TagNumber(1) + set payload(Payload v) { + setField(1, v); + } + + @$pb.TagNumber(1) + $core.bool hasPayload() => $_has(0); + @$pb.TagNumber(1) + void clearPayload() => clearField(1); + @$pb.TagNumber(1) + Payload ensurePayload() => $_ensure(0); + + @$pb.TagNumber(2) + BoolValue get expectCompressed => $_getN(1); + @$pb.TagNumber(2) + set expectCompressed(BoolValue v) { + setField(2, v); + } + + @$pb.TagNumber(2) + $core.bool hasExpectCompressed() => $_has(1); + @$pb.TagNumber(2) + void clearExpectCompressed() => clearField(2); + @$pb.TagNumber(2) + BoolValue ensureExpectCompressed() => $_ensure(1); +} + +class StreamingInputCallResponse extends $pb.GeneratedMessage { + static final $pb.BuilderInfo _i = $pb.BuilderInfo( + const $core.bool.fromEnvironment('protobuf.omit_message_names') + ? '' + : 'StreamingInputCallResponse', + package: const $pb.PackageName( + const $core.bool.fromEnvironment('protobuf.omit_message_names') + ? '' + : 'grpc.testing'), + createEmptyInstance: create) + ..a<$core.int>( + 1, + const $core.bool.fromEnvironment('protobuf.omit_field_names') + ? '' + : 'aggregatedPayloadSize', + $pb.PbFieldType.O3) + ..hasRequiredFields = false; + + StreamingInputCallResponse._() : super(); + factory StreamingInputCallResponse({ + $core.int? aggregatedPayloadSize, + }) { + final _result = create(); + if (aggregatedPayloadSize != null) { + _result.aggregatedPayloadSize = aggregatedPayloadSize; + } + return _result; + } + factory StreamingInputCallResponse.fromBuffer($core.List<$core.int> i, + [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromBuffer(i, r); + factory StreamingInputCallResponse.fromJson($core.String i, + [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromJson(i, r); + @$core.Deprecated('Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.deepCopy] instead. ' + 'Will be removed in next major version') + StreamingInputCallResponse clone() => + StreamingInputCallResponse()..mergeFromMessage(this); + @$core.Deprecated('Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.rebuild] instead. ' + 'Will be removed in next major version') + StreamingInputCallResponse copyWith( + void Function(StreamingInputCallResponse) updates) => + super.copyWith( + (message) => updates(message as StreamingInputCallResponse)) + as StreamingInputCallResponse; // ignore: deprecated_member_use + $pb.BuilderInfo get info_ => _i; + @$core.pragma('dart2js:noInline') + static StreamingInputCallResponse create() => StreamingInputCallResponse._(); + StreamingInputCallResponse createEmptyInstance() => create(); + static $pb.PbList<StreamingInputCallResponse> createRepeated() => + $pb.PbList<StreamingInputCallResponse>(); + @$core.pragma('dart2js:noInline') + static StreamingInputCallResponse getDefault() => _defaultInstance ??= + $pb.GeneratedMessage.$_defaultFor<StreamingInputCallResponse>(create); + static StreamingInputCallResponse? _defaultInstance; + + @$pb.TagNumber(1) + $core.int get aggregatedPayloadSize => $_getIZ(0); + @$pb.TagNumber(1) + set aggregatedPayloadSize($core.int v) { + $_setSignedInt32(0, v); + } + + @$pb.TagNumber(1) + $core.bool hasAggregatedPayloadSize() => $_has(0); + @$pb.TagNumber(1) + void clearAggregatedPayloadSize() => clearField(1); +} + +class ResponseParameters extends $pb.GeneratedMessage { + static final $pb.BuilderInfo _i = $pb.BuilderInfo( + const $core.bool.fromEnvironment('protobuf.omit_message_names') + ? '' + : 'ResponseParameters', + package: const $pb.PackageName( + const $core.bool.fromEnvironment('protobuf.omit_message_names') + ? '' + : 'grpc.testing'), + createEmptyInstance: create) + ..a<$core.int>( + 1, + const $core.bool.fromEnvironment('protobuf.omit_field_names') + ? '' + : 'size', + $pb.PbFieldType.O3) + ..a<$core.int>( + 2, + const $core.bool.fromEnvironment('protobuf.omit_field_names') + ? '' + : 'intervalUs', + $pb.PbFieldType.O3) + ..aOM<BoolValue>( + 3, + const $core.bool.fromEnvironment('protobuf.omit_field_names') + ? '' + : 'compressed', + subBuilder: BoolValue.create) + ..hasRequiredFields = false; + + ResponseParameters._() : super(); + factory ResponseParameters({ + $core.int? size, + $core.int? intervalUs, + BoolValue? compressed, + }) { + final _result = create(); + if (size != null) { + _result.size = size; + } + if (intervalUs != null) { + _result.intervalUs = intervalUs; + } + if (compressed != null) { + _result.compressed = compressed; + } + return _result; + } + factory ResponseParameters.fromBuffer($core.List<$core.int> i, + [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromBuffer(i, r); + factory ResponseParameters.fromJson($core.String i, + [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromJson(i, r); + @$core.Deprecated('Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.deepCopy] instead. ' + 'Will be removed in next major version') + ResponseParameters clone() => ResponseParameters()..mergeFromMessage(this); + @$core.Deprecated('Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.rebuild] instead. ' + 'Will be removed in next major version') + ResponseParameters copyWith(void Function(ResponseParameters) updates) => + super.copyWith((message) => updates(message as ResponseParameters)) + as ResponseParameters; // ignore: deprecated_member_use + $pb.BuilderInfo get info_ => _i; + @$core.pragma('dart2js:noInline') + static ResponseParameters create() => ResponseParameters._(); + ResponseParameters createEmptyInstance() => create(); + static $pb.PbList<ResponseParameters> createRepeated() => + $pb.PbList<ResponseParameters>(); + @$core.pragma('dart2js:noInline') + static ResponseParameters getDefault() => _defaultInstance ??= + $pb.GeneratedMessage.$_defaultFor<ResponseParameters>(create); + static ResponseParameters? _defaultInstance; + + @$pb.TagNumber(1) + $core.int get size => $_getIZ(0); + @$pb.TagNumber(1) + set size($core.int v) { + $_setSignedInt32(0, v); + } + + @$pb.TagNumber(1) + $core.bool hasSize() => $_has(0); + @$pb.TagNumber(1) + void clearSize() => clearField(1); + + @$pb.TagNumber(2) + $core.int get intervalUs => $_getIZ(1); + @$pb.TagNumber(2) + set intervalUs($core.int v) { + $_setSignedInt32(1, v); + } + + @$pb.TagNumber(2) + $core.bool hasIntervalUs() => $_has(1); + @$pb.TagNumber(2) + void clearIntervalUs() => clearField(2); + + @$pb.TagNumber(3) + BoolValue get compressed => $_getN(2); + @$pb.TagNumber(3) + set compressed(BoolValue v) { + setField(3, v); + } + + @$pb.TagNumber(3) + $core.bool hasCompressed() => $_has(2); + @$pb.TagNumber(3) + void clearCompressed() => clearField(3); + @$pb.TagNumber(3) + BoolValue ensureCompressed() => $_ensure(2); +} + +class StreamingOutputCallRequest extends $pb.GeneratedMessage { + static final $pb.BuilderInfo _i = $pb.BuilderInfo( + const $core.bool.fromEnvironment('protobuf.omit_message_names') + ? '' + : 'StreamingOutputCallRequest', + package: const $pb.PackageName( + const $core.bool.fromEnvironment('protobuf.omit_message_names') + ? '' + : 'grpc.testing'), + createEmptyInstance: create) + ..e<PayloadType>( + 1, + const $core.bool.fromEnvironment('protobuf.omit_field_names') + ? '' + : 'responseType', + $pb.PbFieldType.OE, + defaultOrMaker: PayloadType.COMPRESSABLE, + valueOf: PayloadType.valueOf, + enumValues: PayloadType.values) + ..pc<ResponseParameters>( + 2, + const $core.bool.fromEnvironment('protobuf.omit_field_names') + ? '' + : 'responseParameters', + $pb.PbFieldType.PM, + subBuilder: ResponseParameters.create) + ..aOM<Payload>( + 3, + const $core.bool.fromEnvironment('protobuf.omit_field_names') + ? '' + : 'payload', + subBuilder: Payload.create) + ..aOM<EchoStatus>( + 7, + const $core.bool.fromEnvironment('protobuf.omit_field_names') + ? '' + : 'responseStatus', + subBuilder: EchoStatus.create) + ..hasRequiredFields = false; + + StreamingOutputCallRequest._() : super(); + factory StreamingOutputCallRequest({ + PayloadType? responseType, + $core.Iterable<ResponseParameters>? responseParameters, + Payload? payload, + EchoStatus? responseStatus, + }) { + final _result = create(); + if (responseType != null) { + _result.responseType = responseType; + } + if (responseParameters != null) { + _result.responseParameters.addAll(responseParameters); + } + if (payload != null) { + _result.payload = payload; + } + if (responseStatus != null) { + _result.responseStatus = responseStatus; + } + return _result; + } + factory StreamingOutputCallRequest.fromBuffer($core.List<$core.int> i, + [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromBuffer(i, r); + factory StreamingOutputCallRequest.fromJson($core.String i, + [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromJson(i, r); + @$core.Deprecated('Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.deepCopy] instead. ' + 'Will be removed in next major version') + StreamingOutputCallRequest clone() => + StreamingOutputCallRequest()..mergeFromMessage(this); + @$core.Deprecated('Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.rebuild] instead. ' + 'Will be removed in next major version') + StreamingOutputCallRequest copyWith( + void Function(StreamingOutputCallRequest) updates) => + super.copyWith( + (message) => updates(message as StreamingOutputCallRequest)) + as StreamingOutputCallRequest; // ignore: deprecated_member_use + $pb.BuilderInfo get info_ => _i; + @$core.pragma('dart2js:noInline') + static StreamingOutputCallRequest create() => StreamingOutputCallRequest._(); + StreamingOutputCallRequest createEmptyInstance() => create(); + static $pb.PbList<StreamingOutputCallRequest> createRepeated() => + $pb.PbList<StreamingOutputCallRequest>(); + @$core.pragma('dart2js:noInline') + static StreamingOutputCallRequest getDefault() => _defaultInstance ??= + $pb.GeneratedMessage.$_defaultFor<StreamingOutputCallRequest>(create); + static StreamingOutputCallRequest? _defaultInstance; + + @$pb.TagNumber(1) + PayloadType get responseType => $_getN(0); + @$pb.TagNumber(1) + set responseType(PayloadType v) { + setField(1, v); + } + + @$pb.TagNumber(1) + $core.bool hasResponseType() => $_has(0); + @$pb.TagNumber(1) + void clearResponseType() => clearField(1); + + @$pb.TagNumber(2) + $core.List<ResponseParameters> get responseParameters => $_getList(1); + + @$pb.TagNumber(3) + Payload get payload => $_getN(2); + @$pb.TagNumber(3) + set payload(Payload v) { + setField(3, v); + } + + @$pb.TagNumber(3) + $core.bool hasPayload() => $_has(2); + @$pb.TagNumber(3) + void clearPayload() => clearField(3); + @$pb.TagNumber(3) + Payload ensurePayload() => $_ensure(2); + + @$pb.TagNumber(7) + EchoStatus get responseStatus => $_getN(3); + @$pb.TagNumber(7) + set responseStatus(EchoStatus v) { + setField(7, v); + } + + @$pb.TagNumber(7) + $core.bool hasResponseStatus() => $_has(3); + @$pb.TagNumber(7) + void clearResponseStatus() => clearField(7); + @$pb.TagNumber(7) + EchoStatus ensureResponseStatus() => $_ensure(3); +} + +class StreamingOutputCallResponse extends $pb.GeneratedMessage { + static final $pb.BuilderInfo _i = $pb.BuilderInfo( + const $core.bool.fromEnvironment('protobuf.omit_message_names') + ? '' + : 'StreamingOutputCallResponse', + package: const $pb.PackageName( + const $core.bool.fromEnvironment('protobuf.omit_message_names') + ? '' + : 'grpc.testing'), + createEmptyInstance: create) + ..aOM<Payload>( + 1, + const $core.bool.fromEnvironment('protobuf.omit_field_names') + ? '' + : 'payload', + subBuilder: Payload.create) + ..hasRequiredFields = false; + + StreamingOutputCallResponse._() : super(); + factory StreamingOutputCallResponse({ + Payload? payload, + }) { + final _result = create(); + if (payload != null) { + _result.payload = payload; + } + return _result; + } + factory StreamingOutputCallResponse.fromBuffer($core.List<$core.int> i, + [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromBuffer(i, r); + factory StreamingOutputCallResponse.fromJson($core.String i, + [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromJson(i, r); + @$core.Deprecated('Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.deepCopy] instead. ' + 'Will be removed in next major version') + StreamingOutputCallResponse clone() => + StreamingOutputCallResponse()..mergeFromMessage(this); + @$core.Deprecated('Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.rebuild] instead. ' + 'Will be removed in next major version') + StreamingOutputCallResponse copyWith( + void Function(StreamingOutputCallResponse) updates) => + super.copyWith( + (message) => updates(message as StreamingOutputCallResponse)) + as StreamingOutputCallResponse; // ignore: deprecated_member_use + $pb.BuilderInfo get info_ => _i; + @$core.pragma('dart2js:noInline') + static StreamingOutputCallResponse create() => + StreamingOutputCallResponse._(); + StreamingOutputCallResponse createEmptyInstance() => create(); + static $pb.PbList<StreamingOutputCallResponse> createRepeated() => + $pb.PbList<StreamingOutputCallResponse>(); + @$core.pragma('dart2js:noInline') + static StreamingOutputCallResponse getDefault() => _defaultInstance ??= + $pb.GeneratedMessage.$_defaultFor<StreamingOutputCallResponse>(create); + static StreamingOutputCallResponse? _defaultInstance; + + @$pb.TagNumber(1) + Payload get payload => $_getN(0); + @$pb.TagNumber(1) + set payload(Payload v) { + setField(1, v); + } + + @$pb.TagNumber(1) + $core.bool hasPayload() => $_has(0); + @$pb.TagNumber(1) + void clearPayload() => clearField(1); + @$pb.TagNumber(1) + Payload ensurePayload() => $_ensure(0); +} + +class ReconnectParams extends $pb.GeneratedMessage { + static final $pb.BuilderInfo _i = $pb.BuilderInfo( + const $core.bool.fromEnvironment('protobuf.omit_message_names') + ? '' + : 'ReconnectParams', + package: const $pb.PackageName( + const $core.bool.fromEnvironment('protobuf.omit_message_names') + ? '' + : 'grpc.testing'), + createEmptyInstance: create) + ..a<$core.int>( + 1, + const $core.bool.fromEnvironment('protobuf.omit_field_names') + ? '' + : 'maxReconnectBackoffMs', + $pb.PbFieldType.O3) + ..hasRequiredFields = false; + + ReconnectParams._() : super(); + factory ReconnectParams({ + $core.int? maxReconnectBackoffMs, + }) { + final _result = create(); + if (maxReconnectBackoffMs != null) { + _result.maxReconnectBackoffMs = maxReconnectBackoffMs; + } + return _result; + } + factory ReconnectParams.fromBuffer($core.List<$core.int> i, + [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromBuffer(i, r); + factory ReconnectParams.fromJson($core.String i, + [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromJson(i, r); + @$core.Deprecated('Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.deepCopy] instead. ' + 'Will be removed in next major version') + ReconnectParams clone() => ReconnectParams()..mergeFromMessage(this); + @$core.Deprecated('Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.rebuild] instead. ' + 'Will be removed in next major version') + ReconnectParams copyWith(void Function(ReconnectParams) updates) => + super.copyWith((message) => updates(message as ReconnectParams)) + as ReconnectParams; // ignore: deprecated_member_use + $pb.BuilderInfo get info_ => _i; + @$core.pragma('dart2js:noInline') + static ReconnectParams create() => ReconnectParams._(); + ReconnectParams createEmptyInstance() => create(); + static $pb.PbList<ReconnectParams> createRepeated() => + $pb.PbList<ReconnectParams>(); + @$core.pragma('dart2js:noInline') + static ReconnectParams getDefault() => _defaultInstance ??= + $pb.GeneratedMessage.$_defaultFor<ReconnectParams>(create); + static ReconnectParams? _defaultInstance; + + @$pb.TagNumber(1) + $core.int get maxReconnectBackoffMs => $_getIZ(0); + @$pb.TagNumber(1) + set maxReconnectBackoffMs($core.int v) { + $_setSignedInt32(0, v); + } + + @$pb.TagNumber(1) + $core.bool hasMaxReconnectBackoffMs() => $_has(0); + @$pb.TagNumber(1) + void clearMaxReconnectBackoffMs() => clearField(1); +} + +class ReconnectInfo extends $pb.GeneratedMessage { + static final $pb.BuilderInfo _i = $pb.BuilderInfo( + const $core.bool.fromEnvironment('protobuf.omit_message_names') + ? '' + : 'ReconnectInfo', + package: const $pb.PackageName( + const $core.bool.fromEnvironment('protobuf.omit_message_names') + ? '' + : 'grpc.testing'), + createEmptyInstance: create) + ..aOB( + 1, + const $core.bool.fromEnvironment('protobuf.omit_field_names') + ? '' + : 'passed') + ..p<$core.int>( + 2, + const $core.bool.fromEnvironment('protobuf.omit_field_names') + ? '' + : 'backoffMs', + $pb.PbFieldType.P3) + ..hasRequiredFields = false; + + ReconnectInfo._() : super(); + factory ReconnectInfo({ + $core.bool? passed, + $core.Iterable<$core.int>? backoffMs, + }) { + final _result = create(); + if (passed != null) { + _result.passed = passed; + } + if (backoffMs != null) { + _result.backoffMs.addAll(backoffMs); + } + return _result; + } + factory ReconnectInfo.fromBuffer($core.List<$core.int> i, + [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromBuffer(i, r); + factory ReconnectInfo.fromJson($core.String i, + [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromJson(i, r); + @$core.Deprecated('Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.deepCopy] instead. ' + 'Will be removed in next major version') + ReconnectInfo clone() => ReconnectInfo()..mergeFromMessage(this); + @$core.Deprecated('Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.rebuild] instead. ' + 'Will be removed in next major version') + ReconnectInfo copyWith(void Function(ReconnectInfo) updates) => + super.copyWith((message) => updates(message as ReconnectInfo)) + as ReconnectInfo; // ignore: deprecated_member_use + $pb.BuilderInfo get info_ => _i; + @$core.pragma('dart2js:noInline') + static ReconnectInfo create() => ReconnectInfo._(); + ReconnectInfo createEmptyInstance() => create(); + static $pb.PbList<ReconnectInfo> createRepeated() => + $pb.PbList<ReconnectInfo>(); + @$core.pragma('dart2js:noInline') + static ReconnectInfo getDefault() => _defaultInstance ??= + $pb.GeneratedMessage.$_defaultFor<ReconnectInfo>(create); + static ReconnectInfo? _defaultInstance; + + @$pb.TagNumber(1) + $core.bool get passed => $_getBF(0); + @$pb.TagNumber(1) + set passed($core.bool v) { + $_setBool(0, v); + } + + @$pb.TagNumber(1) + $core.bool hasPassed() => $_has(0); + @$pb.TagNumber(1) + void clearPassed() => clearField(1); + + @$pb.TagNumber(2) + $core.List<$core.int> get backoffMs => $_getList(1); +}
diff --git a/grpc/interop/lib/src/generated/messages.pbenum.dart b/grpc/interop/lib/src/generated/messages.pbenum.dart new file mode 100644 index 0000000..f7ba49e --- /dev/null +++ b/grpc/interop/lib/src/generated/messages.pbenum.dart
@@ -0,0 +1,28 @@ +/// +// Generated code. Do not modify. +// source: messages.proto +// +// @dart = 2.12 +// ignore_for_file: annotate_overrides,camel_case_types,unnecessary_const,non_constant_identifier_names,library_prefixes,unused_import,unused_shown_name,return_of_invalid_type,unnecessary_this,prefer_final_fields + +// ignore_for_file: UNDEFINED_SHOWN_NAME +import 'dart:core' as $core; +import 'package:protobuf/protobuf.dart' as $pb; + +class PayloadType extends $pb.ProtobufEnum { + static const PayloadType COMPRESSABLE = PayloadType._( + 0, + const $core.bool.fromEnvironment('protobuf.omit_enum_names') + ? '' + : 'COMPRESSABLE'); + + static const $core.List<PayloadType> values = <PayloadType>[ + COMPRESSABLE, + ]; + + static final $core.Map<$core.int, PayloadType> _byValue = + $pb.ProtobufEnum.initByValue(values); + static PayloadType? valueOf($core.int value) => _byValue[value]; + + const PayloadType._($core.int v, $core.String n) : super(v, n); +}
diff --git a/grpc/interop/lib/src/generated/test.pb.dart b/grpc/interop/lib/src/generated/test.pb.dart new file mode 100644 index 0000000..d7fd8e5 --- /dev/null +++ b/grpc/interop/lib/src/generated/test.pb.dart
@@ -0,0 +1,8 @@ +/// +// Generated code. Do not modify. +// source: test.proto +// +// @dart = 2.12 +// ignore_for_file: annotate_overrides,camel_case_types,unnecessary_const,non_constant_identifier_names,library_prefixes,unused_import,unused_shown_name,return_of_invalid_type,unnecessary_this,prefer_final_fields + +import 'dart:core' as $core;
diff --git a/grpc/interop/lib/src/generated/test.pbgrpc.dart b/grpc/interop/lib/src/generated/test.pbgrpc.dart new file mode 100644 index 0000000..dd5854a --- /dev/null +++ b/grpc/interop/lib/src/generated/test.pbgrpc.dart
@@ -0,0 +1,330 @@ +/// +// Generated code. Do not modify. +// source: test.proto +// +// @dart = 2.12 +// ignore_for_file: annotate_overrides,camel_case_types,unnecessary_const,non_constant_identifier_names,library_prefixes,unused_import,unused_shown_name,return_of_invalid_type,unnecessary_this,prefer_final_fields + +import 'dart:async' as $async; + +import 'dart:core' as $core; + +import 'package:grpc/service_api.dart' as $grpc; +import 'empty.pb.dart' as $0; +import 'messages.pb.dart' as $1; +export 'test.pb.dart'; + +class TestServiceClient extends $grpc.Client { + static final _$emptyCall = $grpc.ClientMethod<$0.Empty, $0.Empty>( + '/grpc.testing.TestService/EmptyCall', + ($0.Empty value) => value.writeToBuffer(), + ($core.List<$core.int> value) => $0.Empty.fromBuffer(value)); + static final _$unaryCall = + $grpc.ClientMethod<$1.SimpleRequest, $1.SimpleResponse>( + '/grpc.testing.TestService/UnaryCall', + ($1.SimpleRequest value) => value.writeToBuffer(), + ($core.List<$core.int> value) => $1.SimpleResponse.fromBuffer(value)); + static final _$cacheableUnaryCall = + $grpc.ClientMethod<$1.SimpleRequest, $1.SimpleResponse>( + '/grpc.testing.TestService/CacheableUnaryCall', + ($1.SimpleRequest value) => value.writeToBuffer(), + ($core.List<$core.int> value) => $1.SimpleResponse.fromBuffer(value)); + static final _$streamingOutputCall = $grpc.ClientMethod< + $1.StreamingOutputCallRequest, $1.StreamingOutputCallResponse>( + '/grpc.testing.TestService/StreamingOutputCall', + ($1.StreamingOutputCallRequest value) => value.writeToBuffer(), + ($core.List<$core.int> value) => + $1.StreamingOutputCallResponse.fromBuffer(value)); + static final _$streamingInputCall = $grpc.ClientMethod< + $1.StreamingInputCallRequest, $1.StreamingInputCallResponse>( + '/grpc.testing.TestService/StreamingInputCall', + ($1.StreamingInputCallRequest value) => value.writeToBuffer(), + ($core.List<$core.int> value) => + $1.StreamingInputCallResponse.fromBuffer(value)); + static final _$fullDuplexCall = $grpc.ClientMethod< + $1.StreamingOutputCallRequest, $1.StreamingOutputCallResponse>( + '/grpc.testing.TestService/FullDuplexCall', + ($1.StreamingOutputCallRequest value) => value.writeToBuffer(), + ($core.List<$core.int> value) => + $1.StreamingOutputCallResponse.fromBuffer(value)); + static final _$halfDuplexCall = $grpc.ClientMethod< + $1.StreamingOutputCallRequest, $1.StreamingOutputCallResponse>( + '/grpc.testing.TestService/HalfDuplexCall', + ($1.StreamingOutputCallRequest value) => value.writeToBuffer(), + ($core.List<$core.int> value) => + $1.StreamingOutputCallResponse.fromBuffer(value)); + static final _$unimplementedCall = $grpc.ClientMethod<$0.Empty, $0.Empty>( + '/grpc.testing.TestService/UnimplementedCall', + ($0.Empty value) => value.writeToBuffer(), + ($core.List<$core.int> value) => $0.Empty.fromBuffer(value)); + + TestServiceClient($grpc.ClientChannel channel, + {$grpc.CallOptions? options, + $core.Iterable<$grpc.ClientInterceptor>? interceptors}) + : super(channel, options: options, interceptors: interceptors); + + $grpc.ResponseFuture<$0.Empty> emptyCall($0.Empty request, + {$grpc.CallOptions? options}) { + return $createUnaryCall(_$emptyCall, request, options: options); + } + + $grpc.ResponseFuture<$1.SimpleResponse> unaryCall($1.SimpleRequest request, + {$grpc.CallOptions? options}) { + return $createUnaryCall(_$unaryCall, request, options: options); + } + + $grpc.ResponseFuture<$1.SimpleResponse> cacheableUnaryCall( + $1.SimpleRequest request, + {$grpc.CallOptions? options}) { + return $createUnaryCall(_$cacheableUnaryCall, request, options: options); + } + + $grpc.ResponseStream<$1.StreamingOutputCallResponse> streamingOutputCall( + $1.StreamingOutputCallRequest request, + {$grpc.CallOptions? options}) { + return $createStreamingCall( + _$streamingOutputCall, $async.Stream.fromIterable([request]), + options: options); + } + + $grpc.ResponseFuture<$1.StreamingInputCallResponse> streamingInputCall( + $async.Stream<$1.StreamingInputCallRequest> request, + {$grpc.CallOptions? options}) { + return $createStreamingCall(_$streamingInputCall, request, options: options) + .single; + } + + $grpc.ResponseStream<$1.StreamingOutputCallResponse> fullDuplexCall( + $async.Stream<$1.StreamingOutputCallRequest> request, + {$grpc.CallOptions? options}) { + return $createStreamingCall(_$fullDuplexCall, request, options: options); + } + + $grpc.ResponseStream<$1.StreamingOutputCallResponse> halfDuplexCall( + $async.Stream<$1.StreamingOutputCallRequest> request, + {$grpc.CallOptions? options}) { + return $createStreamingCall(_$halfDuplexCall, request, options: options); + } + + $grpc.ResponseFuture<$0.Empty> unimplementedCall($0.Empty request, + {$grpc.CallOptions? options}) { + return $createUnaryCall(_$unimplementedCall, request, options: options); + } +} + +abstract class TestServiceBase extends $grpc.Service { + $core.String get $name => 'grpc.testing.TestService'; + + TestServiceBase() { + $addMethod($grpc.ServiceMethod<$0.Empty, $0.Empty>( + 'EmptyCall', + emptyCall_Pre, + false, + false, + ($core.List<$core.int> value) => $0.Empty.fromBuffer(value), + ($0.Empty value) => value.writeToBuffer())); + $addMethod($grpc.ServiceMethod<$1.SimpleRequest, $1.SimpleResponse>( + 'UnaryCall', + unaryCall_Pre, + false, + false, + ($core.List<$core.int> value) => $1.SimpleRequest.fromBuffer(value), + ($1.SimpleResponse value) => value.writeToBuffer())); + $addMethod($grpc.ServiceMethod<$1.SimpleRequest, $1.SimpleResponse>( + 'CacheableUnaryCall', + cacheableUnaryCall_Pre, + false, + false, + ($core.List<$core.int> value) => $1.SimpleRequest.fromBuffer(value), + ($1.SimpleResponse value) => value.writeToBuffer())); + $addMethod($grpc.ServiceMethod<$1.StreamingOutputCallRequest, + $1.StreamingOutputCallResponse>( + 'StreamingOutputCall', + streamingOutputCall_Pre, + false, + true, + ($core.List<$core.int> value) => + $1.StreamingOutputCallRequest.fromBuffer(value), + ($1.StreamingOutputCallResponse value) => value.writeToBuffer())); + $addMethod($grpc.ServiceMethod<$1.StreamingInputCallRequest, + $1.StreamingInputCallResponse>( + 'StreamingInputCall', + streamingInputCall, + true, + false, + ($core.List<$core.int> value) => + $1.StreamingInputCallRequest.fromBuffer(value), + ($1.StreamingInputCallResponse value) => value.writeToBuffer())); + $addMethod($grpc.ServiceMethod<$1.StreamingOutputCallRequest, + $1.StreamingOutputCallResponse>( + 'FullDuplexCall', + fullDuplexCall, + true, + true, + ($core.List<$core.int> value) => + $1.StreamingOutputCallRequest.fromBuffer(value), + ($1.StreamingOutputCallResponse value) => value.writeToBuffer())); + $addMethod($grpc.ServiceMethod<$1.StreamingOutputCallRequest, + $1.StreamingOutputCallResponse>( + 'HalfDuplexCall', + halfDuplexCall, + true, + true, + ($core.List<$core.int> value) => + $1.StreamingOutputCallRequest.fromBuffer(value), + ($1.StreamingOutputCallResponse value) => value.writeToBuffer())); + $addMethod($grpc.ServiceMethod<$0.Empty, $0.Empty>( + 'UnimplementedCall', + unimplementedCall_Pre, + false, + false, + ($core.List<$core.int> value) => $0.Empty.fromBuffer(value), + ($0.Empty value) => value.writeToBuffer())); + } + + $async.Future<$0.Empty> emptyCall_Pre( + $grpc.ServiceCall call, $async.Future<$0.Empty> request) async { + return emptyCall(call, await request); + } + + $async.Future<$1.SimpleResponse> unaryCall_Pre( + $grpc.ServiceCall call, $async.Future<$1.SimpleRequest> request) async { + return unaryCall(call, await request); + } + + $async.Future<$1.SimpleResponse> cacheableUnaryCall_Pre( + $grpc.ServiceCall call, $async.Future<$1.SimpleRequest> request) async { + return cacheableUnaryCall(call, await request); + } + + $async.Stream<$1.StreamingOutputCallResponse> streamingOutputCall_Pre( + $grpc.ServiceCall call, + $async.Future<$1.StreamingOutputCallRequest> request) async* { + yield* streamingOutputCall(call, await request); + } + + $async.Future<$0.Empty> unimplementedCall_Pre( + $grpc.ServiceCall call, $async.Future<$0.Empty> request) async { + return unimplementedCall(call, await request); + } + + $async.Future<$0.Empty> emptyCall($grpc.ServiceCall call, $0.Empty request); + $async.Future<$1.SimpleResponse> unaryCall( + $grpc.ServiceCall call, $1.SimpleRequest request); + $async.Future<$1.SimpleResponse> cacheableUnaryCall( + $grpc.ServiceCall call, $1.SimpleRequest request); + $async.Stream<$1.StreamingOutputCallResponse> streamingOutputCall( + $grpc.ServiceCall call, $1.StreamingOutputCallRequest request); + $async.Future<$1.StreamingInputCallResponse> streamingInputCall( + $grpc.ServiceCall call, + $async.Stream<$1.StreamingInputCallRequest> request); + $async.Stream<$1.StreamingOutputCallResponse> fullDuplexCall( + $grpc.ServiceCall call, + $async.Stream<$1.StreamingOutputCallRequest> request); + $async.Stream<$1.StreamingOutputCallResponse> halfDuplexCall( + $grpc.ServiceCall call, + $async.Stream<$1.StreamingOutputCallRequest> request); + $async.Future<$0.Empty> unimplementedCall( + $grpc.ServiceCall call, $0.Empty request); +} + +class UnimplementedServiceClient extends $grpc.Client { + static final _$unimplementedCall = $grpc.ClientMethod<$0.Empty, $0.Empty>( + '/grpc.testing.UnimplementedService/UnimplementedCall', + ($0.Empty value) => value.writeToBuffer(), + ($core.List<$core.int> value) => $0.Empty.fromBuffer(value)); + + UnimplementedServiceClient($grpc.ClientChannel channel, + {$grpc.CallOptions? options, + $core.Iterable<$grpc.ClientInterceptor>? interceptors}) + : super(channel, options: options, interceptors: interceptors); + + $grpc.ResponseFuture<$0.Empty> unimplementedCall($0.Empty request, + {$grpc.CallOptions? options}) { + return $createUnaryCall(_$unimplementedCall, request, options: options); + } +} + +abstract class UnimplementedServiceBase extends $grpc.Service { + $core.String get $name => 'grpc.testing.UnimplementedService'; + + UnimplementedServiceBase() { + $addMethod($grpc.ServiceMethod<$0.Empty, $0.Empty>( + 'UnimplementedCall', + unimplementedCall_Pre, + false, + false, + ($core.List<$core.int> value) => $0.Empty.fromBuffer(value), + ($0.Empty value) => value.writeToBuffer())); + } + + $async.Future<$0.Empty> unimplementedCall_Pre( + $grpc.ServiceCall call, $async.Future<$0.Empty> request) async { + return unimplementedCall(call, await request); + } + + $async.Future<$0.Empty> unimplementedCall( + $grpc.ServiceCall call, $0.Empty request); +} + +class ReconnectServiceClient extends $grpc.Client { + static final _$start = $grpc.ClientMethod<$1.ReconnectParams, $0.Empty>( + '/grpc.testing.ReconnectService/Start', + ($1.ReconnectParams value) => value.writeToBuffer(), + ($core.List<$core.int> value) => $0.Empty.fromBuffer(value)); + static final _$stop = $grpc.ClientMethod<$0.Empty, $1.ReconnectInfo>( + '/grpc.testing.ReconnectService/Stop', + ($0.Empty value) => value.writeToBuffer(), + ($core.List<$core.int> value) => $1.ReconnectInfo.fromBuffer(value)); + + ReconnectServiceClient($grpc.ClientChannel channel, + {$grpc.CallOptions? options, + $core.Iterable<$grpc.ClientInterceptor>? interceptors}) + : super(channel, options: options, interceptors: interceptors); + + $grpc.ResponseFuture<$0.Empty> start($1.ReconnectParams request, + {$grpc.CallOptions? options}) { + return $createUnaryCall(_$start, request, options: options); + } + + $grpc.ResponseFuture<$1.ReconnectInfo> stop($0.Empty request, + {$grpc.CallOptions? options}) { + return $createUnaryCall(_$stop, request, options: options); + } +} + +abstract class ReconnectServiceBase extends $grpc.Service { + $core.String get $name => 'grpc.testing.ReconnectService'; + + ReconnectServiceBase() { + $addMethod($grpc.ServiceMethod<$1.ReconnectParams, $0.Empty>( + 'Start', + start_Pre, + false, + false, + ($core.List<$core.int> value) => $1.ReconnectParams.fromBuffer(value), + ($0.Empty value) => value.writeToBuffer())); + $addMethod($grpc.ServiceMethod<$0.Empty, $1.ReconnectInfo>( + 'Stop', + stop_Pre, + false, + false, + ($core.List<$core.int> value) => $0.Empty.fromBuffer(value), + ($1.ReconnectInfo value) => value.writeToBuffer())); + } + + $async.Future<$0.Empty> start_Pre( + $grpc.ServiceCall call, $async.Future<$1.ReconnectParams> request) async { + return start(call, await request); + } + + $async.Future<$1.ReconnectInfo> stop_Pre( + $grpc.ServiceCall call, $async.Future<$0.Empty> request) async { + return stop(call, await request); + } + + $async.Future<$0.Empty> start( + $grpc.ServiceCall call, $1.ReconnectParams request); + $async.Future<$1.ReconnectInfo> stop( + $grpc.ServiceCall call, $0.Empty request); +}
diff --git a/grpc/interop/protos/empty.proto b/grpc/interop/protos/empty.proto new file mode 100644 index 0000000..6d0eb93 --- /dev/null +++ b/grpc/interop/protos/empty.proto
@@ -0,0 +1,43 @@ + +// Copyright 2015, Google Inc. +// All rights reserved. +// +// 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 Inc. 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. + +syntax = "proto3"; + +package grpc.testing; + +// An empty message that you can re-use to avoid defining duplicated empty +// messages in your project. A typical example is to use it as argument or the +// return value of a service API. For instance: +// +// service Foo { +// rpc Bar (grpc.testing.Empty) returns (grpc.testing.Empty) { }; +// }; +// +message Empty {}
diff --git a/grpc/interop/protos/messages.proto b/grpc/interop/protos/messages.proto new file mode 100644 index 0000000..a14922a --- /dev/null +++ b/grpc/interop/protos/messages.proto
@@ -0,0 +1,184 @@ + +// Copyright 2015-2016, Google Inc. +// All rights reserved. +// +// 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 Inc. 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. + +// Message definitions to be used by integration test service definitions. + +syntax = "proto3"; + +package grpc.testing; + +// TODO(dgq): Go back to using well-known types once +// https://github.com/grpc/grpc/issues/6980 has been fixed. +// import "google/protobuf/wrappers.proto"; +message BoolValue { + // The bool value. + bool value = 1; +} + +// DEPRECATED, don't use. To be removed shortly. +// The type of payload that should be returned. +enum PayloadType { + // Compressable text format. + COMPRESSABLE = 0; +} + +// A block of data, to simply increase gRPC message size. +message Payload { + // DEPRECATED, don't use. To be removed shortly. + // The type of data in body. + PayloadType type = 1; + // Primary contents of payload. + bytes body = 2; +} + +// A protobuf representation for grpc status. This is used by test +// clients to specify a status that the server should attempt to return. +message EchoStatus { + int32 code = 1; + string message = 2; +} + +// Unary request. +message SimpleRequest { + // DEPRECATED, don't use. To be removed shortly. + // Desired payload type in the response from the server. + // If response_type is RANDOM, server randomly chooses one from other formats. + PayloadType response_type = 1; + + // Desired payload size in the response from the server. + int32 response_size = 2; + + // Optional input payload sent along with the request. + Payload payload = 3; + + // Whether SimpleResponse should include username. + bool fill_username = 4; + + // Whether SimpleResponse should include OAuth scope. + bool fill_oauth_scope = 5; + + // Whether to request the server to compress the response. This field is + // "nullable" in order to interoperate seamlessly with clients not able to + // implement the full compression tests by introspecting the call to verify + // the response's compression status. + BoolValue response_compressed = 6; + + // Whether server should return a given status + EchoStatus response_status = 7; + + // Whether the server should expect this request to be compressed. + BoolValue expect_compressed = 8; +} + +// Unary response, as configured by the request. +message SimpleResponse { + // Payload to increase message size. + Payload payload = 1; + // The user the request came from, for verifying authentication was + // successful when the client expected it. + string username = 2; + // OAuth scope. + string oauth_scope = 3; +} + +// Client-streaming request. +message StreamingInputCallRequest { + // Optional input payload sent along with the request. + Payload payload = 1; + + // Whether the server should expect this request to be compressed. This field + // is "nullable" in order to interoperate seamlessly with servers not able to + // implement the full compression tests by introspecting the call to verify + // the request's compression status. + BoolValue expect_compressed = 2; + + // Not expecting any payload from the response. +} + +// Client-streaming response. +message StreamingInputCallResponse { + // Aggregated size of payloads received from the client. + int32 aggregated_payload_size = 1; +} + +// Configuration for a particular response. +message ResponseParameters { + // Desired payload sizes in responses from the server. + int32 size = 1; + + // Desired interval between consecutive responses in the response stream in + // microseconds. + int32 interval_us = 2; + + // Whether to request the server to compress the response. This field is + // "nullable" in order to interoperate seamlessly with clients not able to + // implement the full compression tests by introspecting the call to verify + // the response's compression status. + BoolValue compressed = 3; +} + +// Server-streaming request. +message StreamingOutputCallRequest { + // DEPRECATED, don't use. To be removed shortly. + // Desired payload type in the response from the server. + // If response_type is RANDOM, the payload from each response in the stream + // might be of different types. This is to simulate a mixed type of payload + // stream. + PayloadType response_type = 1; + + // Configuration for each expected response message. + repeated ResponseParameters response_parameters = 2; + + // Optional input payload sent along with the request. + Payload payload = 3; + + // Whether server should return a given status. + EchoStatus response_status = 7; +} + +// Server-streaming response, as configured by the request and parameters. +message StreamingOutputCallResponse { + // Payload to increase response size. + Payload payload = 1; +} + +// For reconnect interop test only. +// Client tells server what reconnection parameters it used. +message ReconnectParams { + int32 max_reconnect_backoff_ms = 1; +} + +// For reconnect interop test only. +// Server tells client whether its reconnects are following the spec and the +// reconnect backoffs it saw. +message ReconnectInfo { + bool passed = 1; + repeated int32 backoff_ms = 2; +}
diff --git a/grpc/interop/protos/test.proto b/grpc/interop/protos/test.proto new file mode 100644 index 0000000..202276b --- /dev/null +++ b/grpc/interop/protos/test.proto
@@ -0,0 +1,94 @@ + +// Copyright 2015-2016, Google Inc. +// All rights reserved. +// +// 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 Inc. 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. + +// An integration test service that covers all the method signature permutations +// of unary/streaming requests/responses. + +syntax = "proto3"; + +import "empty.proto"; +import "messages.proto"; + +package grpc.testing; + +// A simple service to test the various types of RPCs and experiment with +// performance with various types of payload. +service TestService { + // One empty request followed by one empty response. + rpc EmptyCall(grpc.testing.Empty) returns (grpc.testing.Empty); + + // One request followed by one response. + rpc UnaryCall(SimpleRequest) returns (SimpleResponse); + + // One request followed by one response. Response has cache control + // headers set such that a caching HTTP proxy (such as GFE) can + // satisfy subsequent requests. + rpc CacheableUnaryCall(SimpleRequest) returns (SimpleResponse); + + // One request followed by a sequence of responses (streamed download). + // The server returns the payload with client desired type and sizes. + rpc StreamingOutputCall(StreamingOutputCallRequest) + returns (stream StreamingOutputCallResponse); + + // A sequence of requests followed by one response (streamed upload). + // The server returns the aggregated size of client payload as the result. + rpc StreamingInputCall(stream StreamingInputCallRequest) + returns (StreamingInputCallResponse); + + // A sequence of requests with each request served by the server immediately. + // As one request could lead to multiple responses, this interface + // demonstrates the idea of full duplexing. + rpc FullDuplexCall(stream StreamingOutputCallRequest) + returns (stream StreamingOutputCallResponse); + + // A sequence of requests followed by a sequence of responses. + // The server buffers all the client requests and then serves them in order. A + // stream of responses are returned to the client when the server starts with + // first request. + rpc HalfDuplexCall(stream StreamingOutputCallRequest) + returns (stream StreamingOutputCallResponse); + + // The test server will not implement this method. It will be used + // to test the behavior when clients call unimplemented methods. + rpc UnimplementedCall(grpc.testing.Empty) returns (grpc.testing.Empty); +} + +// A simple service NOT implemented at servers so clients can test for +// that case. +service UnimplementedService { + // A call that no server should implement + rpc UnimplementedCall(grpc.testing.Empty) returns (grpc.testing.Empty); +} + +// A service used to control reconnect server. +service ReconnectService { + rpc Start(grpc.testing.ReconnectParams) returns (grpc.testing.Empty); + rpc Stop(grpc.testing.Empty) returns (grpc.testing.ReconnectInfo); +}
diff --git a/grpc/interop/pubspec.yaml b/grpc/interop/pubspec.yaml new file mode 100644 index 0000000..1caa13e --- /dev/null +++ b/grpc/interop/pubspec.yaml
@@ -0,0 +1,17 @@ +name: interop +description: Dart gRPC interoperability test suite. +publish_to: none + +environment: + sdk: '>=2.12.0 <3.0.0' + +dependencies: + args: ^2.0.0 + async: ^2.2.0 + collection: ^1.14.11 + grpc: + path: ../ + protobuf: ^2.0.0 + +dev_dependencies: + test: ^1.16.0
diff --git a/grpc/interop/server1.key b/grpc/interop/server1.key new file mode 100644 index 0000000..0864629 --- /dev/null +++ b/grpc/interop/server1.key
@@ -0,0 +1,28 @@ +-----BEGIN PRIVATE KEY----- +MIIEvwIBADANBgkqhkiG9w0BAQEFAASCBKkwggSlAgEAAoIBAQDnE443EknxvxBq +6+hvn/t09hl8hx366EBYvZmVM/NC+7igXRAjiJiA/mIaCvL3MS0Iz5hBLxSGICU+ +WproA3GCIFITIwcf/ETyWj/5xpgZ4AKrLrjQmmX8mhwUajfF3UvwMJrCOVqPp67t +PtP+2kBXaqrXdvnvXR41FsIB8V7zIAuIZB6bHQhiGVlc1sgZYsE2EGG9WMmHtS86 +qkAOTjG2XyjmPTGAwhGDpYkYrpzp99IiDh4/Veai81hn0ssQkbry0XRD/Ig3jcHh +23WiriPNJ0JsbgXUSLKRPZObA9VgOLy2aXoN84IMaeK3yy+cwSYG/99w93fUZJte +MXwz4oYZAgMBAAECggEBAIVn2Ncai+4xbH0OLWckabwgyJ4IM9rDc0LIU368O1kU +koais8qP9dujAWgfoh3sGh/YGgKn96VnsZjKHlyMgF+r4TaDJn3k2rlAOWcurGlj +1qaVlsV4HiEzp7pxiDmHhWvp4672Bb6iBG+bsjCUOEk/n9o9KhZzIBluRhtxCmw5 +nw4Do7z00PTvN81260uPWSc04IrytvZUiAIx/5qxD72bij2xJ8t/I9GI8g4FtoVB +8pB6S/hJX1PZhh9VlU6Yk+TOfOVnbebG4W5138LkB835eqk3Zz0qsbc2euoi8Hxi +y1VGwQEmMQ63jXz4c6g+X55ifvUK9Jpn5E8pq+pMd7ECgYEA93lYq+Cr54K4ey5t +sWMa+ye5RqxjzgXj2Kqr55jb54VWG7wp2iGbg8FMlkQwzTJwebzDyCSatguEZLuB +gRGroRnsUOy9vBvhKPOch9bfKIl6qOgzMJB267fBVWx5ybnRbWN/I7RvMQf3k+9y +biCIVnxDLEEYyx7z85/5qxsXg/MCgYEA7wmWKtCTn032Hy9P8OL49T0X6Z8FlkDC +Rk42ygrc/MUbugq9RGUxcCxoImOG9JXUpEtUe31YDm2j+/nbvrjl6/bP2qWs0V7l +dTJl6dABP51pCw8+l4cWgBBX08Lkeen812AAFNrjmDCjX6rHjWHLJcpS18fnRRkP +V1d/AHWX7MMCgYEA6Gsw2guhp0Zf2GCcaNK5DlQab8OL4Hwrpttzo4kuTlwtqNKp +Q9H4al9qfF4Cr1TFya98+EVYf8yFRM3NLNjZpe3gwYf2EerlJj7VLcahw0KKzoN1 +QBENfwgPLRk5sDkx9VhSmcfl/diLroZdpAwtv3vo4nEoxeuGFbKTGx3Qkf0CgYEA +xyR+dcb05Ygm3w4klHQTowQ10s1H80iaUcZBgQuR1ghEtDbUPZHsoR5t1xCB02ys +DgAwLv1bChIvxvH/L6KM8ovZ2LekBX4AviWxoBxJnfz/EVau98B0b1auRN6eSC83 +FRuGldlSOW1z/nSh8ViizSYE5H5HX1qkXEippvFRE88CgYB3Bfu3YQY60ITWIShv +nNkdcbTT9eoP9suaRJjw92Ln+7ZpALYlQMKUZmJ/5uBmLs4RFwUTQruLOPL4yLTH +awADWUzs3IRr1fwn9E+zM8JVyKCnUEM3w4N5UZskGO2klashAd30hWO+knRv/y0r +uGIYs9Ek7YXlXIRVrzMwcsrt1w== +-----END PRIVATE KEY-----
diff --git a/grpc/interop/server1.pem b/grpc/interop/server1.pem new file mode 100644 index 0000000..88244f8 --- /dev/null +++ b/grpc/interop/server1.pem
@@ -0,0 +1,22 @@ +-----BEGIN CERTIFICATE----- +MIIDtDCCApygAwIBAgIUbJfTREJ6k6/+oInWhV1O1j3ZT0IwDQYJKoZIhvcNAQEL +BQAwVjELMAkGA1UEBhMCQVUxEzARBgNVBAgMClNvbWUtU3RhdGUxITAfBgNVBAoM +GEludGVybmV0IFdpZGdpdHMgUHR5IEx0ZDEPMA0GA1UEAwwGdGVzdGNhMB4XDTIw +MDMxODAzMTA0MloXDTMwMDMxNjAzMTA0MlowZTELMAkGA1UEBhMCVVMxETAPBgNV +BAgMCElsbGlub2lzMRAwDgYDVQQHDAdDaGljYWdvMRUwEwYDVQQKDAxFeGFtcGxl +LCBDby4xGjAYBgNVBAMMESoudGVzdC5nb29nbGUuY29tMIIBIjANBgkqhkiG9w0B +AQEFAAOCAQ8AMIIBCgKCAQEA5xOONxJJ8b8Qauvob5/7dPYZfIcd+uhAWL2ZlTPz +Qvu4oF0QI4iYgP5iGgry9zEtCM+YQS8UhiAlPlqa6ANxgiBSEyMHH/xE8lo/+caY +GeACqy640Jpl/JocFGo3xd1L8DCawjlaj6eu7T7T/tpAV2qq13b5710eNRbCAfFe +8yALiGQemx0IYhlZXNbIGWLBNhBhvVjJh7UvOqpADk4xtl8o5j0xgMIRg6WJGK6c +6ffSIg4eP1XmovNYZ9LLEJG68tF0Q/yIN43B4dt1oq4jzSdCbG4F1EiykT2TmwPV +YDi8tml6DfOCDGnit8svnMEmBv/fcPd31GSbXjF8M+KGGQIDAQABo2swaTAJBgNV +HRMEAjAAMAsGA1UdDwQEAwIF4DBPBgNVHREESDBGghAqLnRlc3QuZ29vZ2xlLmZy +ghh3YXRlcnpvb2kudGVzdC5nb29nbGUuYmWCEioudGVzdC55b3V0dWJlLmNvbYcE +wKgBAzANBgkqhkiG9w0BAQsFAAOCAQEAS8hDQA8PSgipgAml7Q3/djwQ644ghWQv +C2Kb+r30RCY1EyKNhnQnIIh/OUbBZvh0M0iYsy6xqXgfDhCB93AA6j0i5cS8fkhH +Jl4RK0tSkGQ3YNY4NzXwQP/vmUgfkw8VBAZ4Y4GKxppdATjffIW+srbAmdDruIRM +wPeikgOoRrXf0LA1fi4TqxARzeRwenQpayNfGHTvVF9aJkl8HoaMunTAdG5pIVcr +9GKi/gEMpXUJbbVv3U5frX1Wo4CFo+rZWJ/LyCMeb0jciNLxSdMwj/E/ZuExlyeZ +gc9ctPjSMvgSyXEKv6Vwobleeg88V2ZgzenziORoWj4KszG/lbQZvg== +-----END CERTIFICATE-----
diff --git a/grpc/interop/tool/regenerate.sh b/grpc/interop/tool/regenerate.sh new file mode 100755 index 0000000..066a0f9 --- /dev/null +++ b/grpc/interop/tool/regenerate.sh
@@ -0,0 +1,6 @@ +#!/usr/bin/env bash +mkdir -p lib/src/generated +protoc --dart_out=grpc:lib/src/generated -Iprotos/ protos/*.proto +rm lib/src/generated/*.pbjson.dart +rm lib/src/generated/{empty,test}.pbenum.dart +dartfmt -w lib/src/generated
diff --git a/grpc/lib/grpc.dart b/grpc/lib/grpc.dart new file mode 100644 index 0000000..09ececc --- /dev/null +++ b/grpc/lib/grpc.dart
@@ -0,0 +1,61 @@ +// Copyright (c) 2017, the gRPC project authors. Please see the AUTHORS file +// for details. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +export 'src/auth/auth.dart' show BaseAuthenticator; +export 'src/auth/auth_io.dart' + show + applicationDefaultCredentialsAuthenticator, + ComputeEngineAuthenticator, + ServiceAccountAuthenticator; +export 'src/client/call.dart' show ClientCall; +export 'src/client/client.dart' show Client; +export 'src/client/client_transport_connector.dart' + show ClientTransportConnector; +export 'src/client/connection.dart' show ConnectionState; +export 'src/client/http2_channel.dart' + show ClientChannel, ClientTransportConnectorChannel; +export 'src/client/interceptor.dart' + show ClientInterceptor, ClientUnaryInvoker, ClientStreamingInvoker; +export 'src/client/method.dart' show ClientMethod; +export 'src/client/options.dart' + show + defaultIdleTimeout, + BackoffStrategy, + defaultBackoffStrategy, + ChannelOptions; +export 'src/client/transport/http2_credentials.dart' + show BadCertificateHandler, allowBadCertificates, ChannelCredentials; + +/// Status detail types +export 'src/generated/google/rpc/error_details.pb.dart'; +export 'src/server/call.dart' show ServiceCall; +export 'src/server/interceptor.dart' show Interceptor; +export 'src/server/server.dart' + show + ServerCredentials, + ServerLocalCredentials, + ServerTlsCredentials, + ConnectionServer, + Server; +export 'src/server/service.dart' show ServiceMethod, Service; +export 'src/shared/api.dart'; +export 'src/shared/codec.dart' show Codec, IdentityCodec, GzipCodec; +export 'src/shared/codec_registry.dart'; +export 'src/shared/message.dart' + show GrpcMessage, GrpcMetadata, GrpcData, grpcDecompressor; +export 'src/shared/security.dart' + show supportedAlpnProtocols, createSecurityContext; +export 'src/shared/streams.dart' show GrpcHttpEncoder, GrpcHttpDecoder; +export 'src/shared/timeout.dart' show toTimeoutString, fromTimeoutString;
diff --git a/grpc/lib/grpc_connection_interface.dart b/grpc/lib/grpc_connection_interface.dart new file mode 100644 index 0000000..37bf3d6 --- /dev/null +++ b/grpc/lib/grpc_connection_interface.dart
@@ -0,0 +1,32 @@ +// Copyright (c) 2020, the gRPC project authors. Please see the AUTHORS file +// for details. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +/// Exports an interface suitable for defining an alternate implementation of +/// [ClientChannel]. + +export 'src/client/call.dart' show CallOptions, ClientCall; +export 'src/client/channel.dart' show ClientChannelBase; +export 'src/client/connection.dart' show ClientConnection; +export 'src/client/http2_channel.dart' show ClientChannel; +export 'src/client/options.dart' show ChannelOptions; +export 'src/client/transport/transport.dart' + show GrpcTransportStream, ErrorHandler; + +export 'src/shared/codec.dart'; +export 'src/shared/codec_registry.dart'; +export 'src/shared/message.dart' show frame, GrpcMessage, grpcDecompressor; +export 'src/shared/status.dart' show GrpcError; +export 'src/shared/streams.dart' show GrpcHttpDecoder; +export 'src/shared/timeout.dart' show toTimeoutString;
diff --git a/grpc/lib/grpc_or_grpcweb.dart b/grpc/lib/grpc_or_grpcweb.dart new file mode 100644 index 0000000..a3eabce --- /dev/null +++ b/grpc/lib/grpc_or_grpcweb.dart
@@ -0,0 +1,85 @@ +// Copyright (c) 2021, the gRPC project authors. Please see the AUTHORS file +// for details. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import 'src/client/grpc_or_grpcweb_channel_grpc.dart' + if (dart.library.html) 'src/client/grpc_or_grpcweb_channel_web.dart'; +import 'src/client/http2_channel.dart'; +import 'src/client/options.dart'; + +export 'src/shared/api.dart'; + +/// A client channel that underneath uses gRPC [ClientChannel] on all platforms +/// except web, on which it uses [GrpcWebClientChannel]. +/// +/// Note that gRPC and gRPC-web are 2 different protocols and server must be +/// able to speak both of them for this to work. +/// Depending on its exact setup, the server may expose gRPC and gRPC-web on the +/// same port (this is the standard setup of AspNetCore implementation for +/// example), on separate ports of the same host (common setup of in-process +/// gRPC-web to gRPC proxies or colocated Envoy gRPC-web to gRPC proxy), or as +/// a completely separate endpoints (for example if Envoy and gRPC server are +/// exposed as different kubernetes services). A corresponding constructor is +/// provided for each case. +class GrpcOrGrpcWebClientChannel extends GrpcOrGrpcWebClientChannelInternal { + GrpcOrGrpcWebClientChannel.toSeparateEndpoints({ + required String grpcHost, + required int grpcPort, + required bool grpcTransportSecure, + required String grpcWebHost, + required int grpcWebPort, + required bool grpcWebTransportSecure, + }) : super( + grpcHost: grpcHost, + grpcPort: grpcPort, + grpcTransportSecure: grpcTransportSecure, + grpcWebHost: grpcWebHost, + grpcWebPort: grpcWebPort, + grpcWebTransportSecure: grpcWebTransportSecure, + ); + + GrpcOrGrpcWebClientChannel.toSeparatePorts({ + required String host, + required int grpcPort, + required bool grpcTransportSecure, + required int grpcWebPort, + required bool grpcWebTransportSecure, + }) : super( + grpcHost: host, + grpcPort: grpcPort, + grpcTransportSecure: grpcTransportSecure, + grpcWebHost: host, + grpcWebPort: grpcWebPort, + grpcWebTransportSecure: grpcWebTransportSecure, + ); + + GrpcOrGrpcWebClientChannel.toSingleEndpoint({ + required String host, + required int port, + required bool transportSecure, + }) : super( + grpcHost: host, + grpcPort: port, + grpcTransportSecure: transportSecure, + grpcWebHost: host, + grpcWebPort: port, + grpcWebTransportSecure: transportSecure, + ); + + GrpcOrGrpcWebClientChannel.grpc( + Object host, { + int port = 443, + ChannelOptions options = const ChannelOptions(), + }) : super.grpc(host, port: port, options: options); +}
diff --git a/grpc/lib/grpc_web.dart b/grpc/lib/grpc_web.dart new file mode 100644 index 0000000..db0a34d --- /dev/null +++ b/grpc/lib/grpc_web.dart
@@ -0,0 +1,20 @@ +// Copyright (c) 2019, the gRPC project authors. Please see the AUTHORS file +// for details. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +export 'src/client/call.dart' show WebCallOptions; + +export 'src/client/web_channel.dart' show GrpcWebClientChannel; + +export 'src/shared/api.dart';
diff --git a/grpc/lib/service_api.dart b/grpc/lib/service_api.dart new file mode 100644 index 0000000..53d056d --- /dev/null +++ b/grpc/lib/service_api.dart
@@ -0,0 +1,29 @@ +// Copyright (c) 2019, the gRPC project authors. Please see the AUTHORS file +// for details. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +/// Exports the minimum api to define server and client stubs. +/// +/// Mainly intended to be imported by generated code. +library service_api; + +export 'src/client/call.dart' show CallOptions, MetadataProvider; +export 'src/client/channel.dart' show ClientChannel; +export 'src/client/client.dart' show Client; +export 'src/client/common.dart' show ResponseFuture, ResponseStream; +export 'src/client/interceptor.dart' + show ClientInterceptor, ClientUnaryInvoker, ClientStreamingInvoker; +export 'src/client/method.dart' show ClientMethod; +export 'src/server/call.dart' show ServiceCall; +export 'src/server/service.dart' show Service, ServiceMethod;
diff --git a/grpc/lib/src/auth/auth.dart b/grpc/lib/src/auth/auth.dart new file mode 100644 index 0000000..a0059fc --- /dev/null +++ b/grpc/lib/src/auth/auth.dart
@@ -0,0 +1,150 @@ +// Copyright (c) 2018, the gRPC project authors. Please see the AUTHORS file +// for details. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import 'dart:async'; +import 'dart:convert'; + +import 'package:googleapis_auth/googleapis_auth.dart' as auth; +import 'package:http/http.dart' as http; + +import '../client/call.dart'; +import 'rsa.dart'; + +const _tokenExpirationThreshold = Duration(seconds: 30); + +abstract class BaseAuthenticator { + auth.AccessToken? _accessToken; + late String _lastUri; + var _lastUriSet = false; + + Future<void> authenticate(Map<String, String> metadata, String uri) async { + if (_accessToken == null || + _accessToken!.hasExpired || + !_lastUriSet || + uri != _lastUri) { + await obtainAccessCredentials(uri); + _lastUri = uri; + _lastUriSet = true; + } + + final auth = '${_accessToken!.type} ${_accessToken!.data}'; + metadata['authorization'] = auth; + + if (_tokenExpiresSoon) { + // Token is about to expire. Extend it prematurely. + obtainAccessCredentials(_lastUri).catchError((_) {}); + } + } + + bool get _tokenExpiresSoon => _accessToken!.expiry + .subtract(_tokenExpirationThreshold) + .isBefore(DateTime.now().toUtc()); + + CallOptions get toCallOptions => CallOptions(providers: [authenticate]); + + Future<void> obtainAccessCredentials(String uri); +} + +abstract class HttpBasedAuthenticator extends BaseAuthenticator { + Future<void>? _call; + + @override + Future<void> obtainAccessCredentials(String uri) { + if (_call == null) { + final authClient = http.Client(); + _call = obtainCredentialsWithClient(authClient, uri).then((credentials) { + _accessToken = credentials.accessToken; + _call = null; + authClient.close(); + }); + } + return _call!; + } + + Future<auth.AccessCredentials> obtainCredentialsWithClient( + http.Client client, String uri); +} + +class JwtServiceAccountAuthenticator extends BaseAuthenticator { + auth.ServiceAccountCredentials _serviceAccountCredentials; + String? _projectId; + String? _keyId; + + JwtServiceAccountAuthenticator.fromJson( + Map<String, dynamic> serviceAccountJson) + : _serviceAccountCredentials = + auth.ServiceAccountCredentials.fromJson(serviceAccountJson), + _projectId = serviceAccountJson['project_id'], + _keyId = serviceAccountJson['private_key_id']; + + factory JwtServiceAccountAuthenticator(String serviceAccountJsonString) => + JwtServiceAccountAuthenticator.fromJson( + jsonDecode(serviceAccountJsonString)); + + String? get projectId => _projectId; + + @override + Future<void> obtainAccessCredentials(String uri) async { + _accessToken = _jwtTokenFor(_serviceAccountCredentials, _keyId, uri); + } +} + +// TODO(jakobr): Expose in googleapis_auth. +auth.AccessToken _jwtTokenFor( + auth.ServiceAccountCredentials credentials, String? keyId, String uri, + {String? user, List<String>? scopes}) { + // Subtracting 20 seconds from current timestamp to allow for clock skew among + // servers. + final timestamp = + (DateTime.now().toUtc().millisecondsSinceEpoch ~/ 1000) - 20; + final expiry = timestamp + 3600; + + final header = <String, String>{'alg': 'RS256', 'typ': 'JWT'}; + if (keyId != null) { + header['kid'] = keyId; + } + + final claims = <String, dynamic>{ + 'iss': credentials.email, + 'aud': uri, + 'exp': expiry, + 'iat': timestamp, + 'sub': user ?? credentials.email + }; + if (scopes != null) { + claims['scope'] = scopes.join(' '); + } + + final headerBase64 = _base64url(ascii.encode(jsonEncode(header))); + final claimsBase64 = _base64url(utf8.encode(jsonEncode(claims))); + + final data = '$headerBase64.$claimsBase64'; + + final key = credentials.privateRSAKey; + // We convert to our internal version of RSAPrivateKey. See rsa.dart for more + // explanation. + final signer = RS256Signer(RSAPrivateKey( + key.n, key.e, key.d, key.p, key.q, key.dmp1, key.dmq1, key.coeff)); + final signature = signer.sign(ascii.encode(data)); + + final jwt = '$data.${_base64url(signature)}'; + + return auth.AccessToken('Bearer', jwt, + DateTime.fromMillisecondsSinceEpoch(expiry * 1000, isUtc: true)); +} + +String _base64url(List<int> bytes) { + return base64Url.encode(bytes).replaceAll('=', ''); +}
diff --git a/grpc/lib/src/auth/auth_io.dart b/grpc/lib/src/auth/auth_io.dart new file mode 100644 index 0000000..ed20cd1 --- /dev/null +++ b/grpc/lib/src/auth/auth_io.dart
@@ -0,0 +1,166 @@ +import 'dart:convert'; +import 'dart:io'; + +import 'package:googleapis_auth/auth_io.dart' as auth; +import 'package:http/http.dart' as http; + +import 'auth.dart'; + +class ComputeEngineAuthenticator extends HttpBasedAuthenticator { + @override + Future<auth.AccessCredentials> obtainCredentialsWithClient( + http.Client client, String uri) => + auth.obtainAccessCredentialsViaMetadataServer(client); +} + +class ServiceAccountAuthenticator extends HttpBasedAuthenticator { + late auth.ServiceAccountCredentials _serviceAccountCredentials; + final List<String> _scopes; + String? _projectId; + + ServiceAccountAuthenticator.fromJson( + Map<String, dynamic> serviceAccountJson, this._scopes) + : _serviceAccountCredentials = + auth.ServiceAccountCredentials.fromJson(serviceAccountJson), + _projectId = serviceAccountJson['project_id']; + + factory ServiceAccountAuthenticator( + String serviceAccountJsonString, List<String> scopes) => + ServiceAccountAuthenticator.fromJson( + jsonDecode(serviceAccountJsonString), scopes); + + String? get projectId => _projectId; + + @override + Future<auth.AccessCredentials> obtainCredentialsWithClient( + http.Client client, String uri) => + auth.obtainAccessCredentialsViaServiceAccount( + _serviceAccountCredentials, _scopes, client); +} + +class _CredentialsRefreshingAuthenticator extends HttpBasedAuthenticator { + final auth.ClientId _clientId; + auth.AccessCredentials _accessCredentials; + final String? _quotaProject; + + _CredentialsRefreshingAuthenticator( + this._clientId, + this._accessCredentials, + this._quotaProject, + ); + + @override + Future<void> authenticate(Map<String, String> metadata, String uri) async { + await super.authenticate(metadata, uri); + if (_quotaProject != null) { + // https://cloud.google.com/apis/docs/system-parameters#definitions + metadata['X-Goog-User-Project'] = _quotaProject!; + } + } + + @override + Future<auth.AccessCredentials> obtainCredentialsWithClient( + http.Client client, + String uri, + ) async { + _accessCredentials = await auth.refreshCredentials( + _clientId, + _accessCredentials, + client, + ); + return _accessCredentials; + } +} + +/// Create an [HttpBasedAuthenticator] using [Application Default Credentials][ADC]. +/// +/// Looks for credentials in the following order of preference: +/// 1. A JSON file whose path is specified by `GOOGLE_APPLICATION_CREDENTIALS`, +/// this file typically contains [exported service account keys][svc-keys]. +/// 2. A JSON file created by [`gcloud auth application-default login`][gcloud-login] +/// in a well-known location (`%APPDATA%/gcloud/application_default_credentials.json` +/// on Windows and `$HOME/.config/gcloud/application_default_credentials.json` on Linux/Mac). +/// 3. On Google Compute Engine and App Engine Flex we fetch credentials from +/// [GCE metadata service][metadata]. +/// +/// [metadata]: https://cloud.google.com/compute/docs/storing-retrieving-metadata +/// [svc-keys]: https://cloud.google.com/docs/authentication/getting-started +/// [gcloud-login]: https://cloud.google.com/sdk/gcloud/reference/auth/application-default/login +/// [ADC]: https://cloud.google.com/docs/authentication/production +Future<HttpBasedAuthenticator> applicationDefaultCredentialsAuthenticator( + List<String> scopes, +) async { + File? credFile; + String? fileSource; + // If env var specifies a file to load credentials from we'll do that. + final credsEnv = Platform.environment['GOOGLE_APPLICATION_CREDENTIALS']; + if (credsEnv != null && credsEnv.isNotEmpty) { + // If env var is specified and not empty, we always try to load, even if + // the file doesn't exist. + credFile = File(credsEnv); + fileSource = 'GOOGLE_APPLICATION_CREDENTIALS'; + } + + // Attempt to use file created by `gcloud auth application-default login` + File gcloudAdcFile; + if (Platform.isWindows) { + gcloudAdcFile = File.fromUri(Uri.directory(Platform.environment['APPDATA']!) + .resolve('gcloud/application_default_credentials.json')); + } else { + gcloudAdcFile = File.fromUri(Uri.directory(Platform.environment['HOME']!) + .resolve('.config/gcloud/application_default_credentials.json')); + } + // Only try to load from gcloudAdcFile if it exists. + if (credFile == null && await gcloudAdcFile.exists()) { + credFile = gcloudAdcFile; + fileSource = '`gcloud auth application-default login`'; + } + + // Attempt to load form credFile, if detected + if (credFile != null) { + var credentials; + try { + credentials = json.decode(await credFile.readAsString()); + } on IOException { + throw Exception( + 'Failed to read credentials file from $fileSource', + ); + } on FormatException { + throw Exception( + 'Failed to parse JSON from credentials file from $fileSource', + ); + } + + if (credentials is Map && credentials['type'] == 'authorized_user') { + final clientId = auth.ClientId( + credentials['client_id'], + credentials['client_secret'], + ); + + final client = http.Client(); + try { + final accessCreds = await auth.refreshCredentials( + clientId, + auth.AccessCredentials( + // Hack: Create empty credentials that have expired. + auth.AccessToken('Bearer', '', DateTime(0).toUtc()), + credentials['refresh_token'], + scopes, + ), + client, + ); + return _CredentialsRefreshingAuthenticator( + clientId, + accessCreds, + credentials['quota_project_id'], + ); + } finally { + client.close(); + } + } + + return ServiceAccountAuthenticator(json.encode(credentials), scopes); + } + + return ComputeEngineAuthenticator(); +}
diff --git a/grpc/lib/src/auth/rsa.dart b/grpc/lib/src/auth/rsa.dart new file mode 100644 index 0000000..a1669b1 --- /dev/null +++ b/grpc/lib/src/auth/rsa.dart
@@ -0,0 +1,321 @@ +// Copyright (c) 2019, the gRPC project authors. Please see the AUTHORS file +// for details. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// This implementation is borrowed from googleapis_auth. +// TODO(https://github.com/dart-lang/build/issues/2322): If this is fixed we can +// go back to doing a src/ import of this. However that is also not ideal. +// TODO(sigurdm): Get rid of this when Dart has enough crypto support. + +import 'dart:typed_data'; + +import 'package:crypto/crypto.dart'; + +/// Used for signing messages with a private RSA key. +/// +/// The implemented algorithm can be seen in +/// RFC 3447, Section 9.2 EMSA-PKCS1-v1_5. +class RS256Signer { + // NIST sha-256 OID (2 16 840 1 101 3 4 2 1) + // See a reference for the encoding here: + // http://msdn.microsoft.com/en-us/library/bb540809%28v=vs.85%29.aspx + static const _RSA_SHA256_ALGORITHM_IDENTIFIER = [ + 0x06, + 0x09, + 0x60, + 0x86, + 0x48, + 0x01, + 0x65, + 0x03, + 0x04, + 0x02, + 0x01 + ]; + + final RSAPrivateKey _rsaKey; + + RS256Signer(this._rsaKey); + + List<int> sign(List<int> bytes) { + final digest = _digestInfo(sha256.convert(bytes).bytes); + final modulusLen = (_rsaKey.bitLength + 7) ~/ 8; + + final block = Uint8List(modulusLen); + final padLength = block.length - digest.length - 3; + block[0] = 0x00; + block[1] = 0x01; + block.fillRange(2, 2 + padLength, 0xFF); + block[2 + padLength] = 0x00; + block.setRange(2 + padLength + 1, block.length, digest); + return RSAAlgorithm.encrypt(_rsaKey, block, modulusLen); + } + + static Uint8List _digestInfo(List<int> hash) { + // DigestInfo :== SEQUENCE { + // digestAlgorithm AlgorithmIdentifier, + // digest OCTET STRING + // } + var offset = 0; + final digestInfo = Uint8List( + 2 + 2 + _RSA_SHA256_ALGORITHM_IDENTIFIER.length + 2 + 2 + hash.length); + { + // DigestInfo + digestInfo[offset++] = ASN1Parser.SEQUENCE_TAG; + digestInfo[offset++] = digestInfo.length - 2; + { + // AlgorithmIdentifier. + digestInfo[offset++] = ASN1Parser.SEQUENCE_TAG; + digestInfo[offset++] = _RSA_SHA256_ALGORITHM_IDENTIFIER.length + 2; + digestInfo.setAll(offset, _RSA_SHA256_ALGORITHM_IDENTIFIER); + offset += _RSA_SHA256_ALGORITHM_IDENTIFIER.length; + digestInfo[offset++] = ASN1Parser.NULL_TAG; + digestInfo[offset++] = 0; + } + digestInfo[offset++] = ASN1Parser.OCTET_STRING_TAG; + digestInfo[offset++] = hash.length; + digestInfo.setAll(offset, hash); + } + return digestInfo; + } +} + +class ASN1Parser { + static const INTEGER_TAG = 0x02; + static const OCTET_STRING_TAG = 0x04; + static const NULL_TAG = 0x05; + static const OBJECT_ID_TAG = 0x06; + static const SEQUENCE_TAG = 0x30; + + static ASN1Object parse(Uint8List bytes) { + Never invalidFormat(String msg) { + throw ArgumentError('Invalid DER encoding: $msg'); + } + + final data = ByteData.view(bytes.buffer); + var offset = 0; + final end = bytes.length; + + void checkNBytesAvailable(int n) { + if ((offset + n) > end) { + invalidFormat('Tried to read more bytes than available.'); + } + } + + List<int> readBytes(int n) { + checkNBytesAvailable(n); + + final integerBytes = bytes.sublist(offset, offset + n); + offset += n; + return integerBytes; + } + + int readEncodedLength() { + checkNBytesAvailable(1); + + final lengthByte = data.getUint8(offset++); + + // Short length encoding form: This byte is the length itself. + if (lengthByte < 0x80) { + return lengthByte; + } + + // Long length encoding form: + // This byte has in bits 0..6 the number of bytes following which encode + // the length. + var countLengthBytes = lengthByte & 0x7f; + checkNBytesAvailable(countLengthBytes); + + var length = 0; + while (countLengthBytes > 0) { + length = (length << 8) | data.getUint8(offset++); + countLengthBytes--; + } + return length; + } + + void readNullBytes() { + checkNBytesAvailable(1); + final nullByte = data.getUint8(offset++); + if (nullByte != 0x00) { + invalidFormat('Null byte expect, but was: $nullByte.'); + } + } + + ASN1Object decodeObject() { + checkNBytesAvailable(1); + final tag = bytes[offset++]; + switch (tag) { + case INTEGER_TAG: + final size = readEncodedLength(); + return ASN1Integer(RSAAlgorithm.bytes2BigInt(readBytes(size))); + case OCTET_STRING_TAG: + final size = readEncodedLength(); + return ASN1OctetString(readBytes(size)); + case NULL_TAG: + readNullBytes(); + return ASN1Null(); + case OBJECT_ID_TAG: + final size = readEncodedLength(); + return ASN1ObjectIdentifier(readBytes(size)); + case SEQUENCE_TAG: + final lengthInBytes = readEncodedLength(); + if ((offset + lengthInBytes) > end) { + invalidFormat('Tried to read more bytes than available.'); + } + final endOfSequence = offset + lengthInBytes; + + final objects = <ASN1Object>[]; + while (offset < endOfSequence) { + objects.add(decodeObject()); + } + return ASN1Sequence(objects); + default: + invalidFormat( + 'Unexpected tag $tag at offset ${offset - 1} (end: $end).'); + } + } + + final obj = decodeObject(); + if (offset != bytes.length) { + throw ArgumentError('More bytes than expected in ASN1 encoding.'); + } + return obj; + } +} + +abstract class ASN1Object {} + +class ASN1Sequence extends ASN1Object { + final List<ASN1Object> objects; + ASN1Sequence(this.objects); +} + +class ASN1Integer extends ASN1Object { + final BigInt integer; + ASN1Integer(this.integer); +} + +class ASN1OctetString extends ASN1Object { + final List<int> bytes; + ASN1OctetString(this.bytes); +} + +class ASN1ObjectIdentifier extends ASN1Object { + final List<int> bytes; + ASN1ObjectIdentifier(this.bytes); +} + +class ASN1Null extends ASN1Object {} + +/// Represents integers obtained while creating a Public/Private key pair. +class RSAPrivateKey { + /// First prime number. + final BigInt p; + + /// Second prime number. + final BigInt q; + + /// Modulus for public and private keys. Satisfies `n=p*q`. + final BigInt n; + + /// Public key exponent. Satisfies `d*e=1 mod phi(n)`. + final BigInt e; + + /// Private key exponent. Satisfies `d*e=1 mod phi(n)`. + final BigInt d; + + /// Different form of [p]. Satisfies `dmp1=d mod (p-1)`. + final BigInt dmp1; + + /// Different form of [p]. Satisfies `dmq1=d mod (q-1)`. + final BigInt dmq1; + + /// A coefficient which satisfies `coeff=q^-1 mod p`. + final BigInt coeff; + + /// The number of bits used for the modulus. Usually 1024, 2048 or 4096 bits. + int get bitLength => n.bitLength; + + RSAPrivateKey( + this.n, this.e, this.d, this.p, this.q, this.dmp1, this.dmq1, this.coeff); +} + +/// Provides a [encrypt] method for encrypting messages with a [RSAPrivateKey]. +abstract class RSAAlgorithm { + /// Performs the encryption of [bytes] with the private [key]. + /// Others who have access to the public key will be able to decrypt this + /// the result. + /// + /// The [intendedLength] argument specifies the number of bytes in which the + /// result should be encoded. Zero bytes will be used for padding. + static List<int> encrypt( + RSAPrivateKey key, List<int> bytes, int intendedLength) { + final message = bytes2BigInt(bytes); + final encryptedMessage = _encryptInteger(key, message); + return integer2Bytes(encryptedMessage, intendedLength); + } + + static BigInt _encryptInteger(RSAPrivateKey key, BigInt x) { + // The following is equivalent to `_modPow(x, key.d, key.n) but is much + // more efficient. It exploits the fact that we have dmp1/dmq1. + var xp = _modPow(x % key.p, key.dmp1, key.p); + final xq = _modPow(x % key.q, key.dmq1, key.q); + while (xp < xq) { + xp += key.p; + } + return ((((xp - xq) * key.coeff) % key.p) * key.q) + xq; + } + + // TODO(kevmoo): see if this can be done more efficiently with BigInt + static BigInt _modPow(BigInt b, BigInt e, BigInt m) { + if (e < BigInt.one) { + return BigInt.one; + } + if (b < BigInt.zero || b > m) { + b = b % m; + } + var r = BigInt.one; + while (e > BigInt.zero) { + if ((e & BigInt.one) > BigInt.zero) { + r = (r * b) % m; + } + e >>= 1; + b = (b * b) % m; + } + return r; + } + + static BigInt bytes2BigInt(List<int> bytes) { + var number = BigInt.zero; + for (var i = 0; i < bytes.length; i++) { + number = (number << 8) | BigInt.from(bytes[i]); + } + return number; + } + + static List<int> integer2Bytes(BigInt integer, int intendedLength) { + if (integer < BigInt.one) { + throw ArgumentError('Only positive integers are supported.'); + } + final bytes = Uint8List(intendedLength); + for (var i = bytes.length - 1; i >= 0; i--) { + bytes[i] = (integer & _bigIntFF).toInt(); + integer >>= 8; + } + return bytes; + } +} + +final _bigIntFF = BigInt.from(0xff);
diff --git a/grpc/lib/src/client/call.dart b/grpc/lib/src/client/call.dart new file mode 100644 index 0000000..f89f68d --- /dev/null +++ b/grpc/lib/src/client/call.dart
@@ -0,0 +1,496 @@ +// Copyright (c) 2018, the gRPC project authors. Please see the AUTHORS file +// for details. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import 'dart:async'; +import 'dart:developer'; + +import '../shared/codec.dart'; +import '../shared/message.dart'; +import '../shared/profiler.dart'; +import '../shared/status.dart'; +import 'common.dart'; +import 'connection.dart'; +import 'method.dart'; +import 'transport/transport.dart'; + +const _reservedHeaders = [ + 'content-type', + 'te', + 'grpc-timeout', + 'grpc-accept-encoding', + 'grpc-encoding', + 'user-agent', +]; + +/// Provides per-RPC metadata. +/// +/// Metadata providers will be invoked for every RPC, and can add their own +/// metadata to the RPC. If the function returns a [Future], the RPC will await +/// completion of the returned [Future] before transmitting the request. +/// +/// The metadata provider is given the current [metadata] map (possibly modified +/// by previous metadata providers) and the [uri] that is being called, and is +/// expected to modify the map before returning or before completing the +/// returned [Future]. +typedef MetadataProvider = FutureOr<void> Function( + Map<String, String> metadata, String uri); + +/// Runtime options for an RPC. +class CallOptions { + final Map<String, String> metadata; + final Duration? timeout; + final List<MetadataProvider> metadataProviders; + final Codec? compression; + + CallOptions._( + this.metadata, + this.timeout, + this.metadataProviders, + this.compression, + ); + + /// Creates a [CallOptions] object. + /// + /// [CallOptions] can specify static [metadata], set the [timeout], and + /// configure per-RPC metadata [providers]. The metadata [providers] are + /// invoked in order for every RPC, and can modify the outgoing metadata + /// (including metadata provided by previous providers). + factory CallOptions({ + Map<String, String>? metadata, + Duration? timeout, + List<MetadataProvider>? providers, + Codec? compression, + }) { + return CallOptions._( + Map.unmodifiable(metadata ?? {}), + timeout, + List.unmodifiable(providers ?? []), + compression, + ); + } + + factory CallOptions.from(Iterable<CallOptions> options) => + options.fold(CallOptions(), (p, o) => p.mergedWith(o)); + + CallOptions mergedWith(CallOptions? other) { + if (other == null) return this; + final mergedMetadata = Map.from(metadata)..addAll(other.metadata); + final mergedTimeout = other.timeout ?? timeout; + final mergedProviders = List.from(metadataProviders) + ..addAll(other.metadataProviders); + final mergedCompression = other.compression ?? compression; + return CallOptions._( + Map.unmodifiable(mergedMetadata), + mergedTimeout, + List.unmodifiable(mergedProviders), + mergedCompression, + ); + } +} + +/// Runtime options for gRPC-web. +class WebCallOptions extends CallOptions { + /// Whether to eliminate the CORS preflight request. + /// + /// If set to [true], all HTTP headers will be packed into an '$httpHeaders' + /// query parameter, which should downgrade complex CORS requests into + /// simple ones. This eliminates an extra roundtrip. + /// + /// For this to work correctly, a proxy server must be set up that + /// understands the query parameter and can unpack/send the original + /// list of headers to the server endpoint. + final bool? bypassCorsPreflight; + + /// Whether to send credentials along with the XHR. + /// + /// This may be required for proxying or wherever the server + /// needs to otherwise inspect client cookies for that domain. + final bool? withCredentials; + // TODO(mightyvoice): add a list of extra QueryParameter for gRPC. + + WebCallOptions._(Map<String, String> metadata, Duration? timeout, + List<MetadataProvider> metadataProviders, + {this.bypassCorsPreflight, this.withCredentials}) + : super._(metadata, timeout, metadataProviders, null); + + /// Creates a [WebCallOptions] object. + /// + /// [WebCallOptions] can specify static [metadata], [timeout], + /// metadata [providers] of [CallOptions], [bypassCorsPreflight] and + /// [withCredentials] for CORS request. + factory WebCallOptions( + {Map<String, String>? metadata, + Duration? timeout, + List<MetadataProvider>? providers, + bool? bypassCorsPreflight, + bool? withCredentials}) { + return WebCallOptions._(Map.unmodifiable(metadata ?? {}), timeout, + List.unmodifiable(providers ?? []), + bypassCorsPreflight: bypassCorsPreflight ?? false, + withCredentials: withCredentials ?? false); + } + + @override + CallOptions mergedWith(CallOptions? other) { + if (other == null) return this; + + final mergedMetadata = Map.from(metadata)..addAll(other.metadata); + final mergedTimeout = other.timeout ?? timeout; + final mergedProviders = List.from(metadataProviders) + ..addAll(other.metadataProviders); + + if (other is! WebCallOptions) { + return WebCallOptions._(Map.unmodifiable(mergedMetadata), mergedTimeout, + List.unmodifiable(mergedProviders), + bypassCorsPreflight: bypassCorsPreflight, + withCredentials: withCredentials); + } + + final mergedBypassCorsPreflight = + other.bypassCorsPreflight ?? bypassCorsPreflight; + final mergedWithCredentials = other.withCredentials ?? withCredentials; + return WebCallOptions._(Map.unmodifiable(mergedMetadata), mergedTimeout, + List.unmodifiable(mergedProviders), + bypassCorsPreflight: mergedBypassCorsPreflight, + withCredentials: mergedWithCredentials); + } +} + +/// An active call to a gRPC endpoint. +class ClientCall<Q, R> implements Response { + final ClientMethod<Q, R> _method; + final Stream<Q> _requests; + final CallOptions options; + + final _headers = Completer<Map<String, String>>(); + final _trailers = Completer<Map<String, String>>(); + bool _hasReceivedResponses = false; + + late Map<String, String> _headerMetadata; + + GrpcTransportStream? _stream; + final _responses = StreamController<R>(); + StreamSubscription<List<int>>? _requestSubscription; + StreamSubscription<GrpcMessage>? _responseSubscription; + + bool isCancelled = false; + Timer? _timeoutTimer; + + final TimelineTask? _requestTimeline; + TimelineTask? _responseTimeline; + + ClientCall(this._method, this._requests, this.options, + [this._requestTimeline]) { + _requestTimeline?.start('gRPC Request: ${_method.path}', arguments: { + 'method': _method.path, + 'timeout': options.timeout?.toString(), + }); + _responses.onListen = _onResponseListen; + if (options.timeout != null) { + _timeoutTimer = Timer(options.timeout!, _onTimedOut); + } + } + + void onConnectionError(error) { + _terminateWithError(GrpcError.unavailable('Error connecting: $error')); + } + + void _terminateWithError(GrpcError error) { + _finishTimelineWithError(error, _requestTimeline); + if (!_responses.isClosed) { + _responses.addError(error); + } + _safeTerminate(); + } + + static Map<String, String> _sanitizeMetadata(Map<String, String> metadata) { + final sanitizedMetadata = <String, String>{}; + metadata.forEach((String key, String value) { + final lowerCaseKey = key.trim().toLowerCase(); + if (!lowerCaseKey.startsWith(':') && + !_reservedHeaders.contains(lowerCaseKey)) { + sanitizedMetadata[lowerCaseKey] = value.trim(); + } + }); + return sanitizedMetadata; + } + +// TODO(sigurdm): Find out why we do this. + static String audiencePath(ClientMethod method) { + final lastSlashPos = method.path.lastIndexOf('/'); + return lastSlashPos == -1 + ? method.path + : method.path.substring(0, lastSlashPos); + } + + void onConnectionReady(ClientConnection connection) { + if (isCancelled) return; + + if (options.metadataProviders.isEmpty) { + _sendRequest(connection, _sanitizeMetadata(options.metadata)); + } else { + final metadata = Map<String, String>.from(options.metadata); + Future.forEach( + options.metadataProviders, + (MetadataProvider provider) => provider(metadata, + '${connection.scheme}://${connection.authority}${audiencePath(_method)}')) + .then((_) => _sendRequest(connection, _sanitizeMetadata(metadata))) + .catchError(_onMetadataProviderError); + } + } + + void _onMetadataProviderError(error) { + _terminateWithError(GrpcError.internal('Error making call: $error')); + } + + void _sendRequest(ClientConnection connection, Map<String, String> metadata) { + late final GrpcTransportStream stream; + try { + stream = connection.makeRequest( + _method.path, + options.timeout, + metadata, + _onRequestError, + callOptions: options, + ); + } catch (e) { + _terminateWithError(GrpcError.unavailable('Error making call: $e')); + return; + } + _requestTimeline?.instant('Request sent', arguments: { + 'metadata': metadata, + }); + _requestSubscription = _requests + .map((data) { + _requestTimeline?.instant('Data sent', arguments: { + 'data': data.toString(), + }); + _requestTimeline?.finish(); + return _method.requestSerializer(data); + }) + .handleError(_onRequestError) + .listen(stream.outgoingMessages.add, + onError: stream.outgoingMessages.addError, + onDone: stream.outgoingMessages.close, + cancelOnError: true); + _stream = stream; + // The response stream might have been listened to before _stream was ready, + // so try setting up the subscription here as well. + _onResponseListen(); + } + + void _finishTimelineWithError(GrpcError error, TimelineTask? timeline) { + timeline?.finish(arguments: { + 'error': error.toString(), + }); + } + + void _onTimedOut() { + final error = GrpcError.deadlineExceeded('Deadline exceeded'); + _finishTimelineWithError(error, _requestTimeline); + _responses.addError(error); + _safeTerminate(); + } + + /// Subscribe to incoming response messages, once [_stream] is available, and + /// the caller has subscribed to the [_responses] stream. + void _onResponseListen() { + if (_stream != null && + _responses.hasListener && + _responseSubscription == null) { + // ignore: cancel_subscriptions + final subscription = _stream!.incomingMessages.listen(_onResponseData, + onError: _onResponseError, + onDone: _onResponseDone, + cancelOnError: true); + if (_responses.isPaused) { + subscription.pause(); + } + _responses.onPause = subscription.pause; + _responses.onResume = subscription.resume; + _responses.onCancel = cancel; + + _responseSubscription = subscription; + } + } + + /// Emit an error response to the user, and tear down this call. + void _responseError(GrpcError error, [StackTrace? stackTrace]) { + _finishTimelineWithError(error, _responseTimeline); + _responses.addError(error, stackTrace); + _timeoutTimer?.cancel(); + _requestSubscription?.cancel(); + _responseSubscription!.cancel(); + _responses.close(); + _stream!.terminate(); + } + + /// If there's an error status then process it as a response error. + void _checkForErrorStatus(Map<String, String> trailers) { + final error = grpcErrorDetailsFromTrailers(trailers); + if (error != null) { + _responseError(error); + } + } + + /// Data handler for responses coming from the server. Handles header/trailer + /// metadata, and forwards response objects to [_responses]. + void _onResponseData(GrpcMessage data) { + if (data is GrpcData) { + if (!_headers.isCompleted) { + _responseError(GrpcError.unimplemented('Received data before headers')); + return; + } + if (_trailers.isCompleted) { + _responseError(GrpcError.unimplemented('Received data after trailers')); + return; + } + try { + final decodedData = _method.responseDeserializer(data.data); + _responseTimeline?.instant('Data received', arguments: { + 'data': decodedData.toString(), + }); + _responses.add(decodedData); + _hasReceivedResponses = true; + } catch (e, s) { + _responseError(GrpcError.dataLoss('Error parsing response'), s); + } + } else if (data is GrpcMetadata) { + if (!_headers.isCompleted) { + _headerMetadata = data.metadata; + if (_requestTimeline != null) { + _responseTimeline = timelineTaskFactory( + parent: _requestTimeline, filterKey: clientTimelineFilterKey); + } + _responseTimeline?.start('gRPC Response'); + _responseTimeline?.instant('Metadata received', arguments: { + 'headers': _headerMetadata.toString(), + }); + _headers.complete(_headerMetadata); + return; + } + if (_trailers.isCompleted) { + _responseError(GrpcError.unimplemented('Received multiple trailers')); + return; + } + final metadata = data.metadata; + _responseTimeline?.instant('Metadata received', arguments: { + 'trailers': metadata.toString(), + }); + _trailers.complete(metadata); + + /// Process status error if necessary + _checkForErrorStatus(metadata); + } else { + _responseError(GrpcError.unimplemented('Unexpected frame received')); + } + } + + /// Handler for response errors. Forward the error to the [_responses] stream, + /// wrapped if necessary. + void _onResponseError(error, StackTrace stackTrace) { + if (error is GrpcError) { + _responseError(error, stackTrace); + return; + } + _responseError(GrpcError.unknown(error.toString()), stackTrace); + } + + /// Handles closure of the response stream. Verifies that server has sent + /// response messages and header/trailer metadata, as necessary. + void _onResponseDone() { + if (!_headers.isCompleted) { + _responseError(GrpcError.unavailable('Did not receive anything')); + return; + } + if (!_trailers.isCompleted) { + if (_hasReceivedResponses) { + // Trailers are required after receiving data. + _responseError(GrpcError.unavailable('Missing trailers')); + return; + } + + // Only received a header frame and no data frames, so the header + // should contain "trailers" as well (Trailers-Only). + _trailers.complete(_headerMetadata); + + /// Process status error if necessary + _checkForErrorStatus(_headerMetadata); + } + _responseTimeline?.finish(); + _timeoutTimer?.cancel(); + _responses.close(); + _responseSubscription!.cancel(); + } + + /// Error handler for the requests stream. Something went wrong while trying + /// to send the request to the server. Abort the request, and forward the + /// error to the user code on the [_responses] stream. + void _onRequestError(error, StackTrace stackTrace) { + if (error is! GrpcError) { + error = GrpcError.unknown(error.toString()); + } + + _finishTimelineWithError(error, _requestTimeline); + _responses.addError(error, stackTrace); + _timeoutTimer?.cancel(); + _responses.close(); + _requestSubscription?.cancel(); + _responseSubscription?.cancel(); + _stream!.terminate(); + } + + Stream<R> get response => _responses.stream; + + @override + Future<Map<String, String>> get headers => _headers.future; + + @override + Future<Map<String, String>> get trailers => _trailers.future; + + @override + Future<void> cancel() { + if (!_responses.isClosed) { + final error = GrpcError.cancelled('Cancelled by client.'); + _responses.addError(error); + _finishTimelineWithError(error, _requestTimeline); + } + return _terminate(); + } + + Future<void> _terminate() async { + isCancelled = true; + _timeoutTimer?.cancel(); + // Don't await _responses.close() here. It'll only complete once the done + // event has been delivered, and it's the caller of this function that is + // reading from responses as well, so we might end up deadlocked. + _responses.close(); + _stream?.terminate(); + final futures = <Future>[]; + if (_requestSubscription != null) { + futures.add(_requestSubscription!.cancel()); + } + if (_responseSubscription != null) { + futures.add(_responseSubscription!.cancel()); + } + await Future.wait(futures); + } + + Future<void> _safeTerminate() async { + try { + await _terminate(); + } catch (_) {} + } +}
diff --git a/grpc/lib/src/client/channel.dart b/grpc/lib/src/client/channel.dart new file mode 100644 index 0000000..9ca3c5a --- /dev/null +++ b/grpc/lib/src/client/channel.dart
@@ -0,0 +1,100 @@ +// Copyright (c) 2017, the gRPC project authors. Please see the AUTHORS file +// for details. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import 'dart:async'; + +import '../shared/profiler.dart'; +import '../shared/status.dart'; + +import 'call.dart'; +import 'connection.dart'; +import 'method.dart'; + +/// A channel to a virtual RPC endpoint. +abstract class ClientChannel { + /// Shuts down this channel. + /// + /// No further RPCs can be made on this channel. RPCs already in progress will + /// be allowed to complete. + Future<void> shutdown(); + + /// Terminates this channel. + /// + /// RPCs already in progress will be terminated. No further RPCs can be made + /// on this channel. + Future<void> terminate(); + + /// Initiates a new RPC on this connection. + ClientCall<Q, R> createCall<Q, R>( + ClientMethod<Q, R> method, Stream<Q> requests, CallOptions options); +} + +/// Auxiliary base class implementing much of ClientChannel. +abstract class ClientChannelBase implements ClientChannel { + // TODO(jakobr): Multiple connections, load balancing. + late ClientConnection _connection; + var _connected = false; + bool _isShutdown = false; + + ClientChannelBase(); + + @override + Future<void> shutdown() async { + if (_isShutdown) return; + _isShutdown = true; + if (_connected) { + await _connection.shutdown(); + } + } + + @override + Future<void> terminate() async { + _isShutdown = true; + if (_connected) { + await _connection.terminate(); + } + } + + ClientConnection createConnection(); + + /// Returns a connection to this [Channel]'s RPC endpoint. + /// + /// The connection may be shared between multiple RPCs. + Future<ClientConnection> getConnection() async { + if (_isShutdown) throw GrpcError.unavailable('Channel shutting down.'); + if (!_connected) { + _connection = createConnection(); + _connected = true; + } + return _connection; + } + + @override + ClientCall<Q, R> createCall<Q, R>( + ClientMethod<Q, R> method, Stream<Q> requests, CallOptions options) { + final call = ClientCall( + method, + requests, + options, + isTimelineLoggingEnabled + ? timelineTaskFactory(filterKey: clientTimelineFilterKey) + : null); + getConnection().then((connection) { + if (call.isCancelled) return; + connection.dispatchCall(call); + }, onError: call.onConnectionError); + return call; + } +}
diff --git a/grpc/lib/src/client/client.dart b/grpc/lib/src/client/client.dart new file mode 100644 index 0000000..1ba6d00 --- /dev/null +++ b/grpc/lib/src/client/client.dart
@@ -0,0 +1,74 @@ +// Copyright (c) 2017, the gRPC project authors. Please see the AUTHORS file +// for details. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import 'call.dart'; +import 'channel.dart'; +import 'common.dart'; +import 'interceptor.dart'; +import 'method.dart'; + +/// Base class for client stubs. +class Client { + final ClientChannel _channel; + final CallOptions _options; + final List<ClientInterceptor> _interceptors; + + /// Interceptors will be applied in direct order before making a request. + Client(this._channel, + {CallOptions? options, Iterable<ClientInterceptor>? interceptors}) + : _options = options ?? CallOptions(), + _interceptors = List.unmodifiable(interceptors ?? Iterable.empty()); + + @Deprecated(r'''This method does not invoke interceptors and is superseded +by $createStreamingCall and $createUnaryCall which invoke interceptors. + +If you are getting this warning in autogenerated protobuf client stubs, +regenerate these stubs using protobuf compiler plugin version 19.2.0 or newer. +''') + ClientCall<Q, R> $createCall<Q, R>( + ClientMethod<Q, R> method, Stream<Q> requests, + {CallOptions? options}) { + return _channel.createCall(method, requests, _options.mergedWith(options)); + } + + ResponseFuture<R> $createUnaryCall<Q, R>(ClientMethod<Q, R> method, Q request, + {CallOptions? options}) { + var invoker = (method, request, options) => ResponseFuture<R>( + _channel.createCall<Q, R>(method, Stream.value(request), options)); + + for (final interceptor in _interceptors.reversed) { + final delegate = invoker; + invoker = (method, request, options) => + interceptor.interceptUnary<Q, R>(method, request, options, delegate); + } + + return invoker(method, request, _options.mergedWith(options)); + } + + ResponseStream<R> $createStreamingCall<Q, R>( + ClientMethod<Q, R> method, Stream<Q> requests, + {CallOptions? options}) { + var invoker = (method, requests, options) => + ResponseStream<R>(_channel.createCall<Q, R>(method, requests, options)); + + for (final interceptor in _interceptors.reversed) { + final delegate = invoker; + invoker = (method, requests, options) => interceptor + .interceptStreaming<Q, R>(method, requests, options, delegate); + } + + return invoker(method, requests, _options.mergedWith(options)); + } +}
diff --git a/grpc/lib/src/client/client_transport_connector.dart b/grpc/lib/src/client/client_transport_connector.dart new file mode 100644 index 0000000..08bfa03 --- /dev/null +++ b/grpc/lib/src/client/client_transport_connector.dart
@@ -0,0 +1,18 @@ +import 'dart:async'; + +import 'package:http2/transport.dart'; + +/// A transport-specific configuration used by gRPC clients to connect. +abstract class ClientTransportConnector { + /// Creates a HTTP/2 client connection. + Future<ClientTransportConnection> connect(); + + /// A future that completes when the client closes or when an error occurs. + Future get done; + + /// Shuts down the connection, which should at least close the client. + void shutdown(); + + /// Header populated from any credentials or hostname information. + String get authority; +}
diff --git a/grpc/lib/src/client/common.dart b/grpc/lib/src/client/common.dart new file mode 100644 index 0000000..901ff1a --- /dev/null +++ b/grpc/lib/src/client/common.dart
@@ -0,0 +1,91 @@ +// Copyright (c) 2017, the gRPC project authors. Please see the AUTHORS file +// for details. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import 'dart:async'; + +import 'package:async/async.dart'; + +import '../shared/status.dart'; +import 'call.dart'; + +/// A gRPC response. +abstract class Response { + /// Header metadata returned from the server. + /// + /// The [headers] future will complete before any response objects become + /// available. If [cancel] is called before the headers are available, the + /// returned future will complete with an error. + Future<Map<String, String>> get headers; + + /// Trailer metadata returned from the server. + /// + /// The [trailers] future will complete after all responses have been received + /// from the server. If [cancel] is called before the trailers are available, + /// the returned future will complete with an error. + Future<Map<String, String>> get trailers; + + /// Cancel this gRPC call. Any remaining request objects will not be sent, and + /// no further responses will be received. + Future<void> cancel(); +} + +/// A gRPC response producing a single value. +class ResponseFuture<R> extends DelegatingFuture<R> + with _ResponseMixin<dynamic, R> { + @override + final ClientCall<dynamic, R> _call; + + static R _ensureOnlyOneResponse<R>(R? previous, R element) { + if (previous != null) { + throw GrpcError.unimplemented('More than one response received'); + } + return element; + } + + static R _ensureOneResponse<R>(R? value) { + if (value == null) throw GrpcError.unimplemented('No responses received'); + return value; + } + + ResponseFuture(this._call) + : super(_call.response + .fold<R?>(null, _ensureOnlyOneResponse) + .then(_ensureOneResponse)); +} + +/// A gRPC response producing a stream of values. +class ResponseStream<R> extends DelegatingStream<R> + with _ResponseMixin<dynamic, R> { + @override + final ClientCall<dynamic, R> _call; + + ResponseStream(this._call) : super(_call.response); + + @override + ResponseFuture<R> get single => ResponseFuture(_call); +} + +abstract class _ResponseMixin<Q, R> implements Response { + ClientCall<Q, R> get _call; + + @override + Future<Map<String, String>> get headers => _call.headers; + + @override + Future<Map<String, String>> get trailers => _call.trailers; + + @override + Future<void> cancel() => _call.cancel(); +}
diff --git a/grpc/lib/src/client/connection.dart b/grpc/lib/src/client/connection.dart new file mode 100644 index 0000000..b6f9d8e --- /dev/null +++ b/grpc/lib/src/client/connection.dart
@@ -0,0 +1,60 @@ +// Copyright (c) 2017, the gRPC project authors. Please see the AUTHORS file +// for details. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import 'call.dart'; + +import 'transport/transport.dart'; + +enum ConnectionState { + /// Actively trying to connect. + connecting, + + /// Connection successfully established. + ready, + + /// Some transient failure occurred, waiting to re-connect. + transientFailure, + + /// Not currently connected, and no pending RPCs. + idle, + + /// Shutting down, no further RPCs allowed. + shutdown +} + +abstract class ClientConnection { + String get authority; + String get scheme; + + /// Put [call] on the queue to be dispatched when the connection is ready. + void dispatchCall(ClientCall call); + + /// Start a request for [path] with [metadata]. + GrpcTransportStream makeRequest(String path, Duration? timeout, + Map<String, String> metadata, ErrorHandler onRequestFailure, + {required CallOptions callOptions}); + + /// Shuts down this connection. + /// + /// No further calls may be made on this connection, but existing calls + /// are allowed to finish. + Future<void> shutdown(); + + /// Terminates this connection. + /// + /// All open calls are terminated immediately, and no further calls may be + /// made on this connection. + Future<void> terminate(); +}
diff --git a/grpc/lib/src/client/grpc_or_grpcweb_channel_grpc.dart b/grpc/lib/src/client/grpc_or_grpcweb_channel_grpc.dart new file mode 100644 index 0000000..ec335e4 --- /dev/null +++ b/grpc/lib/src/client/grpc_or_grpcweb_channel_grpc.dart
@@ -0,0 +1,43 @@ +// Copyright (c) 2021, the gRPC project authors. Please see the AUTHORS file +// for details. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import 'http2_channel.dart'; +import 'options.dart'; +import 'transport/http2_credentials.dart'; + +class GrpcOrGrpcWebClientChannelInternal extends ClientChannel { + GrpcOrGrpcWebClientChannelInternal({ + required String grpcHost, + required int grpcPort, + required bool grpcTransportSecure, + required String grpcWebHost, + required int grpcWebPort, + required bool grpcWebTransportSecure, + }) : super( + grpcHost, + port: grpcPort, + options: ChannelOptions( + credentials: grpcTransportSecure + ? ChannelCredentials.secure() + : ChannelCredentials.insecure(), + ), + ); + + GrpcOrGrpcWebClientChannelInternal.grpc( + Object host, { + required int port, + required ChannelOptions options, + }) : super(host, port: port, options: options); +}
diff --git a/grpc/lib/src/client/grpc_or_grpcweb_channel_web.dart b/grpc/lib/src/client/grpc_or_grpcweb_channel_web.dart new file mode 100644 index 0000000..858b0ef --- /dev/null +++ b/grpc/lib/src/client/grpc_or_grpcweb_channel_web.dart
@@ -0,0 +1,46 @@ +// Copyright (c) 2021, the gRPC project authors. Please see the AUTHORS file +// for details. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import 'options.dart'; +import 'web_channel.dart'; + +class GrpcOrGrpcWebClientChannelInternal extends GrpcWebClientChannel { + GrpcOrGrpcWebClientChannelInternal({ + required String grpcHost, + required int grpcPort, + required bool grpcTransportSecure, + required String grpcWebHost, + required int grpcWebPort, + required bool grpcWebTransportSecure, + }) : super.xhr(Uri( + host: grpcWebHost, + port: grpcWebPort, + scheme: grpcWebTransportSecure ? 'https' : 'http', + )); + + GrpcOrGrpcWebClientChannelInternal.grpc( + Object host, { + required int port, + required ChannelOptions options, + }) : super.xhr( + Uri( + host: host.toString(), + port: port, + scheme: options.credentials.isSecure ? 'https' : 'http'), + ) { + // Do not silently ignore options as caller may expect them to have effects. + throw UnsupportedError('not supported by gRPC-web'); + } +}
diff --git a/grpc/lib/src/client/http2_channel.dart b/grpc/lib/src/client/http2_channel.dart new file mode 100644 index 0000000..eee91fc --- /dev/null +++ b/grpc/lib/src/client/http2_channel.dart
@@ -0,0 +1,57 @@ +// Copyright (c) 2019, the gRPC project authors. Please see the AUTHORS file +// for details. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import 'channel.dart'; +import 'client_transport_connector.dart'; +import 'connection.dart'; +import 'http2_connection.dart' show Http2ClientConnection; +import 'options.dart'; + +/// A channel to a virtual gRPC endpoint. +/// +/// For each RPC, the channel picks a [Http2ClientConnection] to dispatch the call. +/// RPCs on the same channel may be sent to different connections, depending on +/// load balancing settings. +class ClientChannel extends ClientChannelBase { + /// Starts the [Server] with the given options. + /// [address] can be either a [String] or an [InternetAddress], in the latter + /// case it can be a Unix Domain Socket address. + final Object host; + final int port; + final ChannelOptions options; + + ClientChannel(this.host, + {this.port = 443, this.options = const ChannelOptions()}) + : super(); + + @override + ClientConnection createConnection() { + return Http2ClientConnection(host, port, options); + } +} + +class ClientTransportConnectorChannel extends ClientChannelBase { + final ClientTransportConnector transportConnector; + final ChannelOptions options; + + ClientTransportConnectorChannel(this.transportConnector, + {this.options = const ChannelOptions()}); + + @override + ClientConnection createConnection() { + return Http2ClientConnection.fromClientTransportConnector( + transportConnector, options); + } +}
diff --git a/grpc/lib/src/client/http2_connection.dart b/grpc/lib/src/client/http2_connection.dart new file mode 100644 index 0000000..4f7522f --- /dev/null +++ b/grpc/lib/src/client/http2_connection.dart
@@ -0,0 +1,386 @@ +// Copyright (c) 2018, the gRPC project authors. Please see the AUTHORS file +// for details. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import 'dart:async'; +import 'dart:convert'; +import 'dart:io'; + +import 'package:http2/transport.dart'; +import 'package:meta/meta.dart'; + +import '../shared/codec.dart'; +import '../shared/timeout.dart'; +import 'call.dart'; +import 'client_transport_connector.dart'; +import 'connection.dart' hide ClientConnection; +import 'connection.dart' as connection; +import 'options.dart'; +import 'transport/http2_credentials.dart'; +import 'transport/http2_transport.dart'; +import 'transport/transport.dart'; + +class Http2ClientConnection implements connection.ClientConnection { + static final _methodPost = Header.ascii(':method', 'POST'); + static final _schemeHttp = Header.ascii(':scheme', 'http'); + static final _schemeHttps = Header.ascii(':scheme', 'https'); + static final _contentTypeGrpc = + Header.ascii('content-type', 'application/grpc'); + static final _teTrailers = Header.ascii('te', 'trailers'); + + final ChannelOptions options; + + connection.ConnectionState _state = ConnectionState.idle; + + @visibleForTesting + void Function(Http2ClientConnection connection)? onStateChanged; + final _pendingCalls = <ClientCall>[]; + + final ClientTransportConnector _transportConnector; + ClientTransportConnection? _transportConnection; + + /// Used for idle and reconnect timeout, depending on [_state]. + Timer? _timer; + + /// Used for making sure a single connection is not kept alive too long. + final Stopwatch _connectionLifeTimer = Stopwatch(); + + Duration? _currentReconnectDelay; + + Http2ClientConnection(Object host, int port, this.options) + : _transportConnector = _SocketTransportConnector(host, port, options); + + Http2ClientConnection.fromClientTransportConnector( + this._transportConnector, this.options); + + ChannelCredentials get credentials => options.credentials; + + @override + String get authority => _transportConnector.authority; + + @override + String get scheme => options.credentials.isSecure ? 'https' : 'http'; + + ConnectionState get state => _state; + + static const _estimatedRoundTripTime = Duration(milliseconds: 20); + + Future<ClientTransportConnection> connectTransport() async { + final connection = await _transportConnector.connect(); + _transportConnector.done.then((_) => _abandonConnection()); + + // Give the settings settings-frame a bit of time to arrive. + // TODO(sigurdm): This is a hack. The http2 package should expose a way of + // waiting for the settings frame to arrive. + await Future.delayed(_estimatedRoundTripTime); + + if (_state == ConnectionState.shutdown) { + _transportConnector.shutdown(); + throw _ShutdownException(); + } + return connection; + } + + void _connect() { + if (_state != ConnectionState.idle && + _state != ConnectionState.transientFailure) { + return; + } + _setState(ConnectionState.connecting); + connectTransport().then<void>((transport) async { + _currentReconnectDelay = null; + _transportConnection = transport; + _connectionLifeTimer + ..reset() + ..start(); + transport.onActiveStateChanged = _handleActiveStateChanged; + _setState(ConnectionState.ready); + + if (_hasPendingCalls()) { + // Take all pending calls out, and reschedule. + final pendingCalls = _pendingCalls.toList(); + _pendingCalls.clear(); + pendingCalls.forEach(dispatchCall); + } + }).catchError(_handleConnectionFailure); + } + + /// Abandons the current connection if it is unhealthy or has been open for + /// too long. + /// + /// Assumes [_transportConnection] is not `null`. + void _refreshConnectionIfUnhealthy() { + final isHealthy = _transportConnection!.isOpen; + final shouldRefresh = + _connectionLifeTimer.elapsed > options.connectionTimeout; + if (shouldRefresh) { + _transportConnection!.finish(); + } + if (!isHealthy || shouldRefresh) { + _abandonConnection(); + } + } + + @override + void dispatchCall(ClientCall call) { + if (_transportConnection != null) { + _refreshConnectionIfUnhealthy(); + } + switch (_state) { + case ConnectionState.ready: + _startCall(call); + break; + case ConnectionState.shutdown: + _shutdownCall(call); + break; + default: + _pendingCalls.add(call); + if (_state == ConnectionState.idle) { + _connect(); + } + } + } + + @override + GrpcTransportStream makeRequest(String path, Duration? timeout, + Map<String, String> metadata, ErrorHandler onRequestFailure, + {CallOptions? callOptions}) { + final compressionCodec = callOptions?.compression; + final headers = createCallHeaders( + credentials.isSecure, + _transportConnector.authority, + path, + timeout, + metadata, + compressionCodec, + userAgent: options.userAgent, + grpcAcceptEncodings: + (callOptions?.metadata ?? const {})['grpc-accept-encoding'] ?? + options.codecRegistry?.supportedEncodings, + ); + final stream = _transportConnection!.makeRequest(headers); + return Http2TransportStream( + stream, + onRequestFailure, + options.codecRegistry, + compressionCodec, + ); + } + + void _startCall(ClientCall call) { + if (call.isCancelled) return; + call.onConnectionReady(this); + } + + void _failCall(ClientCall call, dynamic error) { + if (call.isCancelled) return; + call.onConnectionError(error); + } + + void _shutdownCall(ClientCall call) { + _failCall(call, 'Connection shutting down.'); + } + + @override + Future<void> shutdown() async { + if (_state == ConnectionState.shutdown) return null; + _setShutdownState(); + await _transportConnection?.finish(); + } + + @override + Future<void> terminate() async { + _setShutdownState(); + await _transportConnection?.terminate(); + } + + void _setShutdownState() { + _setState(ConnectionState.shutdown); + _cancelTimer(); + _pendingCalls.forEach(_shutdownCall); + _pendingCalls.clear(); + } + + void _setState(ConnectionState state) { + _state = state; + onStateChanged?.call(this); + } + + void _handleIdleTimeout() { + if (_timer == null || _state != ConnectionState.ready) return; + _cancelTimer(); + _transportConnection + ?.finish() + .catchError((_) {}); // TODO(jakobr): Log error. + _transportConnection = null; + _setState(ConnectionState.idle); + } + + void _cancelTimer() { + _timer?.cancel(); + _timer = null; + } + + void _handleActiveStateChanged(bool isActive) { + if (isActive) { + _cancelTimer(); + } else { + if (options.idleTimeout != null) { + _timer ??= Timer(options.idleTimeout!, _handleIdleTimeout); + } + } + } + + bool _hasPendingCalls() { + // Get rid of pending calls that have timed out. + _pendingCalls.removeWhere((call) => call.isCancelled); + return _pendingCalls.isNotEmpty; + } + + void _handleConnectionFailure(error) { + _transportConnection = null; + if (_state == ConnectionState.shutdown || _state == ConnectionState.idle) { + return; + } + // TODO(jakobr): Log error. + _cancelTimer(); + _pendingCalls.forEach((call) => _failCall(call, error)); + _pendingCalls.clear(); + _setState(ConnectionState.idle); + } + + void _handleReconnect() { + if (_timer == null || _state != ConnectionState.transientFailure) return; + _cancelTimer(); + _connect(); + } + + void _abandonConnection() { + _cancelTimer(); + _transportConnection = null; + + if (_state == ConnectionState.idle && _state == ConnectionState.shutdown) { + // All good. + return; + } + + // We were not planning to close the socket. + if (!_hasPendingCalls()) { + // No pending calls. Just hop to idle, and wait for a new RPC. + _setState(ConnectionState.idle); + return; + } + + // We have pending RPCs. Reconnect after backoff delay. + _setState(ConnectionState.transientFailure); + _currentReconnectDelay = options.backoffStrategy(_currentReconnectDelay); + _timer = Timer(_currentReconnectDelay!, _handleReconnect); + } + + static List<Header> createCallHeaders( + bool useTls, + String authority, + String path, + Duration? timeout, + Map<String, String>? metadata, + Codec? compressionCodec, { + String? userAgent, + String? grpcAcceptEncodings, + }) { + final headers = [ + _methodPost, + useTls ? _schemeHttps : _schemeHttp, + Header(ascii.encode(':path'), utf8.encode(path)), + Header(ascii.encode(':authority'), utf8.encode(authority)), + if (timeout != null) + Header.ascii('grpc-timeout', toTimeoutString(timeout)), + _contentTypeGrpc, + _teTrailers, + Header.ascii('user-agent', userAgent ?? defaultUserAgent), + if (grpcAcceptEncodings != null) + Header.ascii('grpc-accept-encoding', grpcAcceptEncodings), + if (compressionCodec != null) + Header.ascii('grpc-encoding', compressionCodec.encodingName) + ]; + metadata?.forEach((key, value) { + headers.add(Header(ascii.encode(key), utf8.encode(value))); + }); + return headers; + } +} + +class _SocketTransportConnector implements ClientTransportConnector { + /// Either [InternetAddress] or [String]. + final Object _host; + final int _port; + final ChannelOptions _options; + late Socket _socket; // ignore: close_sinks + + _SocketTransportConnector(this._host, this._port, this._options) + : assert(_host is InternetAddress || _host is String); + + @override + Future<ClientTransportConnection> connect() async { + final securityContext = _options.credentials.securityContext; + _socket = await Socket.connect(_host, _port); + // Don't wait for io buffers to fill up before sending requests. + if (_socket.address.type != InternetAddressType.unix) { + _socket.setOption(SocketOption.tcpNoDelay, true); + } + if (securityContext != null) { + // Todo(sigurdm): We want to pass supportedProtocols: ['h2']. + // http://dartbug.com/37950 + _socket = await SecureSocket.secure(_socket, + // This is not really the host, but the authority to verify the TLC + // connection against. + // + // We don't use `this.authority` here, as that includes the port. + host: _options.credentials.authority ?? _host, + context: securityContext, + onBadCertificate: _validateBadCertificate); + } + + return ClientTransportConnection.viaSocket(_socket); + } + + @override + String get authority { + final host = + _host is String ? _host as String : (_host as InternetAddress).host; + return _options.credentials.authority ?? + (_port == 443 ? host : '$host:$_port'); + } + + @override + Future get done { + ArgumentError.checkNotNull(_socket); + return _socket.done; + } + + @override + void shutdown() { + ArgumentError.checkNotNull(_socket); + _socket.destroy(); + } + + bool _validateBadCertificate(X509Certificate certificate) { + final credentials = _options.credentials; + final validator = credentials.onBadCertificate; + + if (validator == null) return false; + return validator(certificate, authority); + } +} + +class _ShutdownException implements Exception {}
diff --git a/grpc/lib/src/client/interceptor.dart b/grpc/lib/src/client/interceptor.dart new file mode 100644 index 0000000..131105b --- /dev/null +++ b/grpc/lib/src/client/interceptor.dart
@@ -0,0 +1,32 @@ +import 'call.dart'; +import 'common.dart'; +import 'method.dart'; + +typedef ClientUnaryInvoker<Q, R> = ResponseFuture<R> Function( + ClientMethod<Q, R> method, Q request, CallOptions options); + +typedef ClientStreamingInvoker<Q, R> = ResponseStream<R> Function( + ClientMethod<Q, R> method, Stream<Q> requests, CallOptions options); + +/// ClientInterceptors intercepts client calls before they are executed. +/// +/// Invoker either calls next interceptor in the chain or performs the call if it is last in chain. +/// To modify [CallOptions] make a clone using [CallOptions.mergedWith]. +abstract class ClientInterceptor { + // Intercept unary call. + // This method is called when client sends single request and receives single response. + ResponseFuture<R> interceptUnary<Q, R>(ClientMethod<Q, R> method, Q request, + CallOptions options, ClientUnaryInvoker<Q, R> invoker) { + return invoker(method, request, options); + } + + // Intercept streaming call. + // This method is called when client sends either request or response stream. + ResponseStream<R> interceptStreaming<Q, R>( + ClientMethod<Q, R> method, + Stream<Q> requests, + CallOptions options, + ClientStreamingInvoker<Q, R> invoker) { + return invoker(method, requests, options); + } +}
diff --git a/grpc/lib/src/client/method.dart b/grpc/lib/src/client/method.dart new file mode 100644 index 0000000..dca28eb --- /dev/null +++ b/grpc/lib/src/client/method.dart
@@ -0,0 +1,23 @@ +// Copyright (c) 2017, the gRPC project authors. Please see the AUTHORS file +// for details. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +/// Description of a gRPC method. +class ClientMethod<Q, R> { + final String path; + final List<int> Function(Q value) requestSerializer; + final R Function(List<int> value) responseDeserializer; + + ClientMethod(this.path, this.requestSerializer, this.responseDeserializer); +}
diff --git a/grpc/lib/src/client/options.dart b/grpc/lib/src/client/options.dart new file mode 100644 index 0000000..bf0ddfb --- /dev/null +++ b/grpc/lib/src/client/options.dart
@@ -0,0 +1,64 @@ +// Copyright (c) 2017, the gRPC project authors. Please see the AUTHORS file +// for details. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import 'dart:math'; + +import '../shared/codec_registry.dart'; +import 'transport/http2_credentials.dart'; + +const defaultIdleTimeout = Duration(minutes: 5); + +/// It seems like Google's gRPC endpoints will forcefully close the +/// connection after precisely 1 hour. So we *proactively* refresh our +/// connection after 50 minutes. This will avoid one failed RPC call. +const defaultConnectionTimeOut = Duration(minutes: 50); +const defaultUserAgent = 'dart-grpc/2.0.0'; + +typedef BackoffStrategy = Duration Function(Duration? lastBackoff); + +// Backoff algorithm from https://github.com/grpc/grpc/blob/master/doc/connection-backoff.md +const _initialBackoff = Duration(seconds: 1); +const _maxBackoff = Duration(seconds: 120); +const _multiplier = 1.6; +const _jitter = 0.2; +final _random = Random(); + +Duration defaultBackoffStrategy(Duration? lastBackoff) { + if (lastBackoff == null) return _initialBackoff; + final jitter = _random.nextDouble() * 2 * _jitter - _jitter; + final nextBackoff = lastBackoff * (_multiplier + jitter); + return nextBackoff < _maxBackoff ? nextBackoff : _maxBackoff; +} + +/// Options controlling how connections are made on a [ClientChannel]. +class ChannelOptions { + final ChannelCredentials credentials; + final Duration? idleTimeout; + final CodecRegistry? codecRegistry; + + /// The maximum time a single connection will be used for new requests. + final Duration connectionTimeout; + final BackoffStrategy backoffStrategy; + final String userAgent; + + const ChannelOptions({ + this.credentials = const ChannelCredentials.secure(), + this.idleTimeout = defaultIdleTimeout, + this.userAgent = defaultUserAgent, + this.backoffStrategy = defaultBackoffStrategy, + this.connectionTimeout = defaultConnectionTimeOut, + this.codecRegistry, + }); +}
diff --git a/grpc/lib/src/client/query_parameter.dart b/grpc/lib/src/client/query_parameter.dart new file mode 100644 index 0000000..2bf3f21 --- /dev/null +++ b/grpc/lib/src/client/query_parameter.dart
@@ -0,0 +1,78 @@ +// Copyright 2018 Google Inc. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +/// Enclosing class for query parameters that can accommodate a [List] of +/// [String] values. +/// +/// Note: though query param values can be [String], [int], or [Int64], this +/// simple class always expects a [String]. This is to avoid [dynamic] typing, +/// plus the whole thing will be converted to [String] later anyway. +class QueryParameter implements Comparable<QueryParameter> { + /// The parameter key. + final String key; + + /// The list of parameter values. + final List<String> values; + + /// Convenience method for single-value params. + String get value => values.first; + + /// Creates an instance by wrapping the single value in a [List]. + QueryParameter(this.key, String value) : values = [value] { + ArgumentError.checkNotNull(key); + if (key.trim().isEmpty) { + throw ArgumentError(key); + } + } + + /// Creates an instance from a [List] of values. + /// + /// This is not the default constructor since the single-value case is the + /// most common. + QueryParameter.multi(this.key, List<String> values) + : values = values..sort() { + ArgumentError.checkNotNull(key); + if (key.trim().isEmpty) { + throw ArgumentError(key); + } + } + + /// Returns the escaped value of the param as will appear in a URL. + @override + String toString() => values.map(_encode).join('&'); + + /// Returns the encoded version of a single key-value pair. + String _encode(String value) { + final safeKey = Uri.encodeQueryComponent(key); + final safeVal = Uri.encodeQueryComponent(value); + return '$safeKey=$safeVal'; + } + + /// Compares this to another [QueryParameter] based on its string value. + @override + int compareTo(QueryParameter other) => toString().compareTo(other.toString()); + + static String buildQuery(List<QueryParameter> queryParams) { + if (queryParams.isEmpty) { + return ''; + } + final buf = StringBuffer(); + // Sorts the query params to ensure a canonical path. + final sortedParamValues = queryParams + ..sort() + ..map((param) => param.toString()); + buf.writeAll(sortedParamValues, '&'); + return buf.toString(); + } +}
diff --git a/grpc/lib/src/client/transport/cors.dart b/grpc/lib/src/client/transport/cors.dart new file mode 100644 index 0000000..07763e4 --- /dev/null +++ b/grpc/lib/src/client/transport/cors.dart
@@ -0,0 +1,49 @@ +// Copyright 2018 Google Inc. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Provides CORS support for HTTP based RPC requests. + +/// The default URL parameter name to overwrite http headers with a URL param +/// to avoid CORS preflight. +/// +/// This comes from the JS impl details at +/// https://github.com/whatwg/fetch/issues/210#issue-129531743. +const _httpHeadersParamName = r'$httpHeaders'; + +// TODO(mightyvoice): Add the const parameter name for HTTP method if not +// always use POST for gRPC-web. + +/// Manipulates the path and headers of the http request such that it will +/// not trigger a CORS preflight request and returns the new path with extra +/// query parameters. +/// +/// A proxy server that understands the '$httpHeaders' query parameter +/// is required for this to work correctly. +Uri moveHttpHeadersToQueryParam(Map<String, String> metadata, Uri requestUri) { + // Nothing to do if there are no headers. + if (metadata.isEmpty) { + return requestUri; + } + + final paramValue = _generateHttpHeadersOverwriteParam(metadata); + metadata.clear(); + return requestUri.replace( + queryParameters: Map.of(requestUri.queryParameters) + ..[_httpHeadersParamName] = paramValue); +} + +/// Generates the URL parameter value with custom headers encoded as +/// HTTP/1.1 headers block. +String _generateHttpHeadersOverwriteParam(Map<String, String> headers) => + headers.entries.map((e) => '${e.key}:${e.value}\r\n').join();
diff --git a/grpc/lib/src/client/transport/http2_credentials.dart b/grpc/lib/src/client/transport/http2_credentials.dart new file mode 100644 index 0000000..5b20e7a --- /dev/null +++ b/grpc/lib/src/client/transport/http2_credentials.dart
@@ -0,0 +1,68 @@ +// Copyright (c) 2018, the gRPC project authors. Please see the AUTHORS file +// for details. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import 'dart:io'; + +import '../../shared/security.dart'; + +/// Handler for checking certificates that fail validation. If this handler +/// returns `true`, the bad certificate is allowed, and the TLS handshake can +/// continue. If the handler returns `false`, the TLS handshake fails, and the +/// connection is aborted. +typedef BadCertificateHandler = bool Function( + X509Certificate certificate, String host); + +/// Bad certificate handler that disables all certificate checks. +/// DO NOT USE IN PRODUCTION! +/// Can be used during development and testing to accept self-signed +/// certificates, etc. +bool allowBadCertificates(X509Certificate certificate, String host) => true; + +/// Options controlling TLS security settings on a [ClientChannel]. +class ChannelCredentials { + final bool isSecure; + final String? authority; + final List<int>? _certificateBytes; + final String? _certificatePassword; + final BadCertificateHandler? onBadCertificate; + + const ChannelCredentials._(this.isSecure, this._certificateBytes, + this._certificatePassword, this.authority, this.onBadCertificate); + + /// Disable TLS. RPCs are sent in clear text. + const ChannelCredentials.insecure({String? authority}) + : this._(false, null, null, authority, null); + + /// Enable TLS and optionally specify the [certificates] to trust. If + /// [certificates] is not provided, the default trust store is used. + const ChannelCredentials.secure( + {List<int>? certificates, + String? password, + String? authority, + BadCertificateHandler? onBadCertificate}) + : this._(true, certificates, password, authority, onBadCertificate); + + SecurityContext? get securityContext { + if (!isSecure) return null; + if (_certificateBytes != null) { + return createSecurityContext(false) + ..setTrustedCertificatesBytes(_certificateBytes!, + password: _certificatePassword); + } + final context = SecurityContext(withTrustedRoots: true); + context.setAlpnProtocols(supportedAlpnProtocols, false); + return context; + } +}
diff --git a/grpc/lib/src/client/transport/http2_transport.dart b/grpc/lib/src/client/transport/http2_transport.dart new file mode 100644 index 0000000..a55c539 --- /dev/null +++ b/grpc/lib/src/client/transport/http2_transport.dart
@@ -0,0 +1,59 @@ +// Copyright (c) 2018, the gRPC project authors. Please see the AUTHORS file +// for details. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import 'dart:async'; + +import 'package:http2/transport.dart'; + +import '../../shared/codec.dart'; +import '../../shared/codec_registry.dart'; +import '../../shared/message.dart'; +import '../../shared/streams.dart'; +import 'transport.dart'; + +class Http2TransportStream extends GrpcTransportStream { + final TransportStream _transportStream; + @override + final Stream<GrpcMessage> incomingMessages; + final StreamController<List<int>> _outgoingMessages = StreamController(); + final ErrorHandler _onError; + + @override + StreamSink<List<int>> get outgoingMessages => _outgoingMessages.sink; + + Http2TransportStream( + this._transportStream, + this._onError, + CodecRegistry? codecRegistry, + Codec? compression, + ) : incomingMessages = _transportStream.incomingMessages + .transform(GrpcHttpDecoder(forResponse: true)) + .transform(grpcDecompressor(codecRegistry: codecRegistry)) { + _outgoingMessages.stream + .map((payload) => frame(payload, compression)) + .map<StreamMessage>((bytes) => DataStreamMessage(bytes)) + .handleError(_onError) + .listen(_transportStream.outgoingMessages.add, + onError: _transportStream.outgoingMessages.addError, + onDone: _transportStream.outgoingMessages.close, + cancelOnError: true); + } + + @override + Future<void> terminate() async { + await _outgoingMessages.close(); + _transportStream.terminate(); + } +}
diff --git a/grpc/lib/src/client/transport/transport.dart b/grpc/lib/src/client/transport/transport.dart new file mode 100644 index 0000000..7ac70a1 --- /dev/null +++ b/grpc/lib/src/client/transport/transport.dart
@@ -0,0 +1,30 @@ +// Copyright (c) 2018, the gRPC project authors. Please see the AUTHORS file +// for details. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import 'dart:async'; + +import '../../shared/message.dart'; + +typedef SocketClosedHandler = void Function(); +typedef ActiveStateHandler = void Function(bool isActive); +typedef ErrorHandler = void Function(Object, StackTrace); + +abstract class GrpcTransportStream { + Stream<GrpcMessage> get incomingMessages; + + StreamSink<List<int>> get outgoingMessages; + + Future<void> terminate(); +}
diff --git a/grpc/lib/src/client/transport/web_streams.dart b/grpc/lib/src/client/transport/web_streams.dart new file mode 100644 index 0000000..7fa9c77 --- /dev/null +++ b/grpc/lib/src/client/transport/web_streams.dart
@@ -0,0 +1,158 @@ +// Copyright (c) 2019, the gRPC project authors. Please see the AUTHORS file +// for details. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import 'dart:convert'; +import 'dart:math'; +import 'dart:typed_data'; + +import '../../shared/message.dart'; +import '../../shared/status.dart'; + +enum _GrpcWebParseState { Init, Length, Message } + +class GrpcWebDecoder extends Converter<ByteBuffer, GrpcMessage> { + @override + GrpcMessage convert(ByteBuffer input) { + final sink = GrpcMessageSink(); + startChunkedConversion(sink) + ..add(input) + ..close(); + return sink.message; + } + + @override + Sink<ByteBuffer> startChunkedConversion(Sink<GrpcMessage> sink) { + return _GrpcWebConversionSink(sink); + } +} + +class _GrpcWebConversionSink extends ChunkedConversionSink<ByteBuffer> { + static const int frameTypeData = 0x00; + static const int frameTypeTrailers = 0x80; + + final Sink<GrpcMessage> _out; + + final _dataHeader = Uint8List(4); + + _GrpcWebParseState _state = _GrpcWebParseState.Init; + var _chunkOffset = 0; + int? _frameType; + var _dataOffset = 0; + Uint8List? _data; + + _GrpcWebConversionSink(this._out); + + int _parseFrameType(List<int> chunkData) { + final frameType = chunkData[_chunkOffset]; + _chunkOffset++; + if (frameType != frameTypeData && frameType != frameTypeTrailers) { + throw GrpcError.unimplemented('Invalid frame type: ${frameType}'); + } + _state = _GrpcWebParseState.Length; + return frameType; + } + + void _parseLength(List<int> chunkData) { + final chunkLength = chunkData.length; + + final headerRemaining = _dataHeader.lengthInBytes - _dataOffset; + final chunkRemaining = chunkLength - _chunkOffset; + final toCopy = min(headerRemaining, chunkRemaining); + _dataHeader.setRange( + _dataOffset, _dataOffset + toCopy, chunkData, _chunkOffset); + _dataOffset += toCopy; + _chunkOffset += toCopy; + if (_dataOffset == _dataHeader.lengthInBytes) { + final dataLength = _dataHeader.buffer.asByteData().getUint32(0); + _dataOffset = 0; + _state = _GrpcWebParseState.Message; + _data = Uint8List(dataLength); + if (dataLength == 0) { + // empty message + _finishMessage(); + } + } + } + + void _parseMessage(List<int> chunkData) { + final dataRemaining = _data!.lengthInBytes - _dataOffset; + if (dataRemaining > 0) { + final chunkRemaining = chunkData.length - _chunkOffset; + final toCopy = min(dataRemaining, chunkRemaining); + _data! + .setRange(_dataOffset, _dataOffset + toCopy, chunkData, _chunkOffset); + _dataOffset += toCopy; + _chunkOffset += toCopy; + } + if (_dataOffset == _data!.lengthInBytes) { + _finishMessage(); + } + } + + void _finishMessage() { + switch (_frameType) { + case frameTypeData: + _out.add(GrpcData(_data!, isCompressed: false)); + break; + case frameTypeTrailers: + final stringData = String.fromCharCodes(_data!); + final headers = _parseHttp1Headers(stringData); + _out.add(GrpcMetadata(headers)); + break; + } + _state = _GrpcWebParseState.Init; + _data = null; + _dataOffset = 0; + } + + Map<String, String> _parseHttp1Headers(String stringData) { + final trimmed = stringData.trim(); + final chunks = trimmed == '' ? <String>[] : trimmed.split('\r\n'); + final headers = <String, String>{}; + for (final chunk in chunks) { + final pos = chunk.indexOf(':'); + headers[chunk.substring(0, pos).trim()] = chunk.substring(pos + 1).trim(); + } + return headers; + } + + @override + void add(ByteBuffer chunk) { + _chunkOffset = 0; + final chunkData = chunk.asUint8List(); + while (_chunkOffset < chunk.lengthInBytes) { + switch (_state) { + case _GrpcWebParseState.Init: + _frameType = _parseFrameType(chunkData); + break; + case _GrpcWebParseState.Length: + _parseLength(chunkData); + break; + case _GrpcWebParseState.Message: + _parseMessage(chunkData); + break; + } + } + _chunkOffset = 0; + } + + @override + void close() { + if (_data != null || _dataOffset != 0) { + throw GrpcError.unavailable('Closed in non-idle state'); + } + _out.close(); + } +}
diff --git a/grpc/lib/src/client/transport/xhr_transport.dart b/grpc/lib/src/client/transport/xhr_transport.dart new file mode 100644 index 0000000..41a5845 --- /dev/null +++ b/grpc/lib/src/client/transport/xhr_transport.dart
@@ -0,0 +1,229 @@ +// Copyright (c) 2018, the gRPC project authors. Please see the AUTHORS file +// for details. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import 'dart:async'; +import 'dart:html'; +import 'dart:typed_data'; + +import 'package:meta/meta.dart'; + +import '../../client/call.dart'; +import '../../shared/message.dart'; +import '../../shared/status.dart'; +import '../connection.dart'; +import 'cors.dart' as cors; +import 'transport.dart'; +import 'web_streams.dart'; + +const _contentTypeKey = 'Content-Type'; + +class XhrTransportStream implements GrpcTransportStream { + final HttpRequest _request; + final ErrorHandler _onError; + final Function(XhrTransportStream stream) _onDone; + bool _headersReceived = false; + int _requestBytesRead = 0; + final StreamController<ByteBuffer> _incomingProcessor = StreamController(); + final StreamController<GrpcMessage> _incomingMessages = StreamController(); + final StreamController<List<int>> _outgoingMessages = StreamController(); + + @override + Stream<GrpcMessage> get incomingMessages => _incomingMessages.stream; + + @override + StreamSink<List<int>> get outgoingMessages => _outgoingMessages.sink; + + XhrTransportStream(this._request, + {required ErrorHandler onError, required onDone}) + : _onError = onError, + _onDone = onDone { + _outgoingMessages.stream + .map(frame) + .listen((data) => _request.send(data), cancelOnError: true); + + _request.onReadyStateChange.listen((data) { + if (_incomingProcessor.isClosed) { + return; + } + switch (_request.readyState) { + case HttpRequest.HEADERS_RECEIVED: + _onHeadersReceived(); + break; + case HttpRequest.DONE: + _onRequestDone(); + _close(); + break; + } + }); + + _request.onError.listen((ProgressEvent event) { + if (_incomingProcessor.isClosed) { + return; + } + _onError(GrpcError.unavailable('XhrConnection connection-error'), + StackTrace.current); + terminate(); + }); + + _request.onProgress.listen((_) { + if (_incomingProcessor.isClosed) { + return; + } + // Use response over responseText as most browsers don't support + // using responseText during an onProgress event. + final responseString = _request.response as String; + final bytes = Uint8List.fromList( + responseString.substring(_requestBytesRead).codeUnits) + .buffer; + _requestBytesRead = responseString.length; + _incomingProcessor.add(bytes); + }); + + _incomingProcessor.stream + .transform(GrpcWebDecoder()) + .transform(grpcDecompressor()) + .listen(_incomingMessages.add, + onError: _onError, onDone: _incomingMessages.close); + } + + bool _validateResponseState() { + try { + validateHttpStatusAndContentType( + _request.status, _request.responseHeaders, + rawResponse: _request.responseText); + return true; + } catch (e, st) { + _onError(e, st); + return false; + } + } + + void _onHeadersReceived() { + _headersReceived = true; + if (!_validateResponseState()) { + return; + } + _incomingMessages.add(GrpcMetadata(_request.responseHeaders)); + } + + void _onRequestDone() { + if (!_headersReceived && !_validateResponseState()) { + return; + } + if (_request.response == null) { + _onError( + GrpcError.unavailable('XhrConnection request null response', null, + _request.responseText), + StackTrace.current); + return; + } + } + + void _close() { + _incomingProcessor.close(); + _outgoingMessages.close(); + _onDone(this); + } + + @override + Future<void> terminate() async { + _close(); + _request.abort(); + } +} + +class XhrClientConnection extends ClientConnection { + final Uri uri; + + final _requests = <XhrTransportStream>{}; + + XhrClientConnection(this.uri); + + @override + String get authority => uri.authority; + @override + String get scheme => uri.scheme; + + void _initializeRequest(HttpRequest request, Map<String, String> metadata) { + for (final header in metadata.keys) { + request.setRequestHeader(header, metadata[header]!); + } + // Overriding the mimetype allows us to stream and parse the data + request.overrideMimeType('text/plain; charset=x-user-defined'); + request.responseType = 'text'; + } + + @visibleForTesting + HttpRequest createHttpRequest() => HttpRequest(); + + @override + GrpcTransportStream makeRequest(String path, Duration? timeout, + Map<String, String> metadata, ErrorHandler onError, + {CallOptions? callOptions}) { + // gRPC-web headers. + if (_getContentTypeHeader(metadata) == null) { + metadata['Content-Type'] = 'application/grpc-web+proto'; + metadata['X-User-Agent'] = 'grpc-web-dart/0.1'; + metadata['X-Grpc-Web'] = '1'; + } + + var requestUri = uri.resolve(path); + if (callOptions is WebCallOptions && + callOptions.bypassCorsPreflight == true) { + requestUri = cors.moveHttpHeadersToQueryParam(metadata, requestUri); + } + + final request = createHttpRequest(); + request.open('POST', requestUri.toString()); + if (callOptions is WebCallOptions && callOptions.withCredentials == true) { + request.withCredentials = true; + } + // Must set headers after calling open(). + _initializeRequest(request, metadata); + + final transportStream = + XhrTransportStream(request, onError: onError, onDone: _removeStream); + _requests.add(transportStream); + return transportStream; + } + + void _removeStream(XhrTransportStream stream) { + _requests.remove(stream); + } + + @override + Future<void> terminate() async { + for (var request in List.of(_requests)) { + request.terminate(); + } + } + + @override + void dispatchCall(ClientCall call) { + call.onConnectionReady(this); + } + + @override + Future<void> shutdown() async {} +} + +MapEntry<String, String>? _getContentTypeHeader(Map<String, String> metadata) { + for (var entry in metadata.entries) { + if (entry.key.toLowerCase() == _contentTypeKey.toLowerCase()) { + return entry; + } + } + return null; +}
diff --git a/grpc/lib/src/client/web_channel.dart b/grpc/lib/src/client/web_channel.dart new file mode 100644 index 0000000..f81da14 --- /dev/null +++ b/grpc/lib/src/client/web_channel.dart
@@ -0,0 +1,30 @@ +// Copyright (c) 2019, the gRPC project authors. Please see the AUTHORS file +// for details. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import 'channel.dart'; +import 'connection.dart'; +import 'transport/xhr_transport.dart'; + +/// A channel to a grpc-web endpoint. +class GrpcWebClientChannel extends ClientChannelBase { + final Uri uri; + + GrpcWebClientChannel.xhr(this.uri) : super(); + + @override + ClientConnection createConnection() { + return XhrClientConnection(uri); + } +}
diff --git a/grpc/lib/src/generated/google/protobuf/any.pb.dart b/grpc/lib/src/generated/google/protobuf/any.pb.dart new file mode 100644 index 0000000..81fcf72 --- /dev/null +++ b/grpc/lib/src/generated/google/protobuf/any.pb.dart
@@ -0,0 +1,113 @@ +/// +// Generated code. Do not modify. +// source: google/protobuf/any.proto +// +// @dart = 2.12 +// ignore_for_file: annotate_overrides,camel_case_types,unnecessary_const,non_constant_identifier_names,library_prefixes,unused_import,unused_shown_name,return_of_invalid_type,unnecessary_this,prefer_final_fields + +import 'dart:core' as $core; + +import 'package:protobuf/protobuf.dart' as $pb; + +import 'package:protobuf/src/protobuf/mixins/well_known.dart' as $mixin; + +class Any extends $pb.GeneratedMessage with $mixin.AnyMixin { + static final $pb.BuilderInfo _i = $pb.BuilderInfo( + const $core.bool.fromEnvironment('protobuf.omit_message_names') + ? '' + : 'Any', + package: const $pb.PackageName( + const $core.bool.fromEnvironment('protobuf.omit_message_names') + ? '' + : 'google.protobuf'), + createEmptyInstance: create, + toProto3Json: $mixin.AnyMixin.toProto3JsonHelper, + fromProto3Json: $mixin.AnyMixin.fromProto3JsonHelper) + ..aOS( + 1, + const $core.bool.fromEnvironment('protobuf.omit_field_names') + ? '' + : 'typeUrl') + ..a<$core.List<$core.int>>( + 2, + const $core.bool.fromEnvironment('protobuf.omit_field_names') + ? '' + : 'value', + $pb.PbFieldType.OY) + ..hasRequiredFields = false; + + Any._() : super(); + factory Any({ + $core.String? typeUrl, + $core.List<$core.int>? value, + }) { + final _result = create(); + if (typeUrl != null) { + _result.typeUrl = typeUrl; + } + if (value != null) { + _result.value = value; + } + return _result; + } + factory Any.fromBuffer($core.List<$core.int> i, + [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromBuffer(i, r); + factory Any.fromJson($core.String i, + [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromJson(i, r); + @$core.Deprecated('Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.deepCopy] instead. ' + 'Will be removed in next major version') + Any clone() => Any()..mergeFromMessage(this); + @$core.Deprecated('Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.rebuild] instead. ' + 'Will be removed in next major version') + Any copyWith(void Function(Any) updates) => + super.copyWith((message) => updates(message as Any)) + as Any; // ignore: deprecated_member_use + $pb.BuilderInfo get info_ => _i; + @$core.pragma('dart2js:noInline') + static Any create() => Any._(); + Any createEmptyInstance() => create(); + static $pb.PbList<Any> createRepeated() => $pb.PbList<Any>(); + @$core.pragma('dart2js:noInline') + static Any getDefault() => + _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor<Any>(create); + static Any? _defaultInstance; + + @$pb.TagNumber(1) + $core.String get typeUrl => $_getSZ(0); + @$pb.TagNumber(1) + set typeUrl($core.String v) { + $_setString(0, v); + } + + @$pb.TagNumber(1) + $core.bool hasTypeUrl() => $_has(0); + @$pb.TagNumber(1) + void clearTypeUrl() => clearField(1); + + @$pb.TagNumber(2) + $core.List<$core.int> get value => $_getN(1); + @$pb.TagNumber(2) + set value($core.List<$core.int> v) { + $_setBytes(1, v); + } + + @$pb.TagNumber(2) + $core.bool hasValue() => $_has(1); + @$pb.TagNumber(2) + void clearValue() => clearField(2); + + /// Creates a new [Any] encoding [message]. + /// + /// The [typeUrl] will be [typeUrlPrefix]/`fullName` where `fullName` is + /// the fully qualified name of the type of [message]. + static Any pack($pb.GeneratedMessage message, + {$core.String typeUrlPrefix = 'type.googleapis.com'}) { + final result = create(); + $mixin.AnyMixin.packIntoAny(result, message, typeUrlPrefix: typeUrlPrefix); + return result; + } +}
diff --git a/grpc/lib/src/generated/google/protobuf/any.pbenum.dart b/grpc/lib/src/generated/google/protobuf/any.pbenum.dart new file mode 100644 index 0000000..e03f029 --- /dev/null +++ b/grpc/lib/src/generated/google/protobuf/any.pbenum.dart
@@ -0,0 +1,6 @@ +/// +// Generated code. Do not modify. +// source: google/protobuf/any.proto +// +// @dart = 2.12 +// ignore_for_file: annotate_overrides,camel_case_types,unnecessary_const,non_constant_identifier_names,library_prefixes,unused_import,unused_shown_name,return_of_invalid_type,unnecessary_this,prefer_final_fields
diff --git a/grpc/lib/src/generated/google/protobuf/any.pbjson.dart b/grpc/lib/src/generated/google/protobuf/any.pbjson.dart new file mode 100644 index 0000000..696648f --- /dev/null +++ b/grpc/lib/src/generated/google/protobuf/any.pbjson.dart
@@ -0,0 +1,16 @@ +/// +// Generated code. Do not modify. +// source: google/protobuf/any.proto +// +// @dart = 2.12 +// ignore_for_file: annotate_overrides,camel_case_types,unnecessary_const,non_constant_identifier_names,library_prefixes,unused_import,unused_shown_name,return_of_invalid_type,unnecessary_this,prefer_final_fields + +import 'dart:core' as $core; + +const Any$json = const { + '1': 'Any', + '2': const [ + const {'1': 'type_url', '3': 1, '4': 1, '5': 9, '10': 'typeUrl'}, + const {'1': 'value', '3': 2, '4': 1, '5': 12, '10': 'value'}, + ], +};
diff --git a/grpc/lib/src/generated/google/protobuf/duration.pb.dart b/grpc/lib/src/generated/google/protobuf/duration.pb.dart new file mode 100644 index 0000000..0733876 --- /dev/null +++ b/grpc/lib/src/generated/google/protobuf/duration.pb.dart
@@ -0,0 +1,103 @@ +/// +// Generated code. Do not modify. +// source: google/protobuf/duration.proto +// +// @dart = 2.12 +// ignore_for_file: annotate_overrides,camel_case_types,unnecessary_const,non_constant_identifier_names,library_prefixes,unused_import,unused_shown_name,return_of_invalid_type,unnecessary_this,prefer_final_fields + +import 'dart:core' as $core; + +import 'package:fixnum/fixnum.dart' as $fixnum; +import 'package:protobuf/protobuf.dart' as $pb; + +import 'package:protobuf/src/protobuf/mixins/well_known.dart' as $mixin; + +class Duration extends $pb.GeneratedMessage with $mixin.DurationMixin { + static final $pb.BuilderInfo _i = $pb.BuilderInfo( + const $core.bool.fromEnvironment('protobuf.omit_message_names') + ? '' + : 'Duration', + package: const $pb.PackageName( + const $core.bool.fromEnvironment('protobuf.omit_message_names') + ? '' + : 'google.protobuf'), + createEmptyInstance: create, + toProto3Json: $mixin.DurationMixin.toProto3JsonHelper, + fromProto3Json: $mixin.DurationMixin.fromProto3JsonHelper) + ..aInt64( + 1, + const $core.bool.fromEnvironment('protobuf.omit_field_names') + ? '' + : 'seconds') + ..a<$core.int>( + 2, + const $core.bool.fromEnvironment('protobuf.omit_field_names') + ? '' + : 'nanos', + $pb.PbFieldType.O3) + ..hasRequiredFields = false; + + Duration._() : super(); + factory Duration({ + $fixnum.Int64? seconds, + $core.int? nanos, + }) { + final _result = create(); + if (seconds != null) { + _result.seconds = seconds; + } + if (nanos != null) { + _result.nanos = nanos; + } + return _result; + } + factory Duration.fromBuffer($core.List<$core.int> i, + [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromBuffer(i, r); + factory Duration.fromJson($core.String i, + [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromJson(i, r); + @$core.Deprecated('Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.deepCopy] instead. ' + 'Will be removed in next major version') + Duration clone() => Duration()..mergeFromMessage(this); + @$core.Deprecated('Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.rebuild] instead. ' + 'Will be removed in next major version') + Duration copyWith(void Function(Duration) updates) => + super.copyWith((message) => updates(message as Duration)) + as Duration; // ignore: deprecated_member_use + $pb.BuilderInfo get info_ => _i; + @$core.pragma('dart2js:noInline') + static Duration create() => Duration._(); + Duration createEmptyInstance() => create(); + static $pb.PbList<Duration> createRepeated() => $pb.PbList<Duration>(); + @$core.pragma('dart2js:noInline') + static Duration getDefault() => + _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor<Duration>(create); + static Duration? _defaultInstance; + + @$pb.TagNumber(1) + $fixnum.Int64 get seconds => $_getI64(0); + @$pb.TagNumber(1) + set seconds($fixnum.Int64 v) { + $_setInt64(0, v); + } + + @$pb.TagNumber(1) + $core.bool hasSeconds() => $_has(0); + @$pb.TagNumber(1) + void clearSeconds() => clearField(1); + + @$pb.TagNumber(2) + $core.int get nanos => $_getIZ(1); + @$pb.TagNumber(2) + set nanos($core.int v) { + $_setSignedInt32(1, v); + } + + @$pb.TagNumber(2) + $core.bool hasNanos() => $_has(1); + @$pb.TagNumber(2) + void clearNanos() => clearField(2); +}
diff --git a/grpc/lib/src/generated/google/protobuf/duration.pbenum.dart b/grpc/lib/src/generated/google/protobuf/duration.pbenum.dart new file mode 100644 index 0000000..af4119e --- /dev/null +++ b/grpc/lib/src/generated/google/protobuf/duration.pbenum.dart
@@ -0,0 +1,6 @@ +/// +// Generated code. Do not modify. +// source: google/protobuf/duration.proto +// +// @dart = 2.12 +// ignore_for_file: annotate_overrides,camel_case_types,unnecessary_const,non_constant_identifier_names,library_prefixes,unused_import,unused_shown_name,return_of_invalid_type,unnecessary_this,prefer_final_fields
diff --git a/grpc/lib/src/generated/google/protobuf/duration.pbjson.dart b/grpc/lib/src/generated/google/protobuf/duration.pbjson.dart new file mode 100644 index 0000000..9b45a6b --- /dev/null +++ b/grpc/lib/src/generated/google/protobuf/duration.pbjson.dart
@@ -0,0 +1,16 @@ +/// +// Generated code. Do not modify. +// source: google/protobuf/duration.proto +// +// @dart = 2.12 +// ignore_for_file: annotate_overrides,camel_case_types,unnecessary_const,non_constant_identifier_names,library_prefixes,unused_import,unused_shown_name,return_of_invalid_type,unnecessary_this,prefer_final_fields + +import 'dart:core' as $core; + +const Duration$json = const { + '1': 'Duration', + '2': const [ + const {'1': 'seconds', '3': 1, '4': 1, '5': 3, '10': 'seconds'}, + const {'1': 'nanos', '3': 2, '4': 1, '5': 5, '10': 'nanos'}, + ], +};
diff --git a/grpc/lib/src/generated/google/rpc/code.pb.dart b/grpc/lib/src/generated/google/rpc/code.pb.dart new file mode 100644 index 0000000..7f4e72c --- /dev/null +++ b/grpc/lib/src/generated/google/rpc/code.pb.dart
@@ -0,0 +1,10 @@ +/// +// Generated code. Do not modify. +// source: google/rpc/code.proto +// +// @dart = 2.12 +// ignore_for_file: annotate_overrides,camel_case_types,unnecessary_const,non_constant_identifier_names,library_prefixes,unused_import,unused_shown_name,return_of_invalid_type,unnecessary_this,prefer_final_fields + +import 'dart:core' as $core; + +export 'code.pbenum.dart';
diff --git a/grpc/lib/src/generated/google/rpc/code.pbenum.dart b/grpc/lib/src/generated/google/rpc/code.pbenum.dart new file mode 100644 index 0000000..8103531 --- /dev/null +++ b/grpc/lib/src/generated/google/rpc/code.pbenum.dart
@@ -0,0 +1,121 @@ +/// +// Generated code. Do not modify. +// source: google/rpc/code.proto +// +// @dart = 2.12 +// ignore_for_file: annotate_overrides,camel_case_types,unnecessary_const,non_constant_identifier_names,library_prefixes,unused_import,unused_shown_name,return_of_invalid_type,unnecessary_this,prefer_final_fields + +// ignore_for_file: UNDEFINED_SHOWN_NAME +import 'dart:core' as $core; +import 'package:protobuf/protobuf.dart' as $pb; + +class Code extends $pb.ProtobufEnum { + static const Code OK = Code._(0, + const $core.bool.fromEnvironment('protobuf.omit_enum_names') ? '' : 'OK'); + static const Code CANCELLED = Code._( + 1, + const $core.bool.fromEnvironment('protobuf.omit_enum_names') + ? '' + : 'CANCELLED'); + static const Code UNKNOWN = Code._( + 2, + const $core.bool.fromEnvironment('protobuf.omit_enum_names') + ? '' + : 'UNKNOWN'); + static const Code INVALID_ARGUMENT = Code._( + 3, + const $core.bool.fromEnvironment('protobuf.omit_enum_names') + ? '' + : 'INVALID_ARGUMENT'); + static const Code DEADLINE_EXCEEDED = Code._( + 4, + const $core.bool.fromEnvironment('protobuf.omit_enum_names') + ? '' + : 'DEADLINE_EXCEEDED'); + static const Code NOT_FOUND = Code._( + 5, + const $core.bool.fromEnvironment('protobuf.omit_enum_names') + ? '' + : 'NOT_FOUND'); + static const Code ALREADY_EXISTS = Code._( + 6, + const $core.bool.fromEnvironment('protobuf.omit_enum_names') + ? '' + : 'ALREADY_EXISTS'); + static const Code PERMISSION_DENIED = Code._( + 7, + const $core.bool.fromEnvironment('protobuf.omit_enum_names') + ? '' + : 'PERMISSION_DENIED'); + static const Code UNAUTHENTICATED = Code._( + 16, + const $core.bool.fromEnvironment('protobuf.omit_enum_names') + ? '' + : 'UNAUTHENTICATED'); + static const Code RESOURCE_EXHAUSTED = Code._( + 8, + const $core.bool.fromEnvironment('protobuf.omit_enum_names') + ? '' + : 'RESOURCE_EXHAUSTED'); + static const Code FAILED_PRECONDITION = Code._( + 9, + const $core.bool.fromEnvironment('protobuf.omit_enum_names') + ? '' + : 'FAILED_PRECONDITION'); + static const Code ABORTED = Code._( + 10, + const $core.bool.fromEnvironment('protobuf.omit_enum_names') + ? '' + : 'ABORTED'); + static const Code OUT_OF_RANGE = Code._( + 11, + const $core.bool.fromEnvironment('protobuf.omit_enum_names') + ? '' + : 'OUT_OF_RANGE'); + static const Code UNIMPLEMENTED = Code._( + 12, + const $core.bool.fromEnvironment('protobuf.omit_enum_names') + ? '' + : 'UNIMPLEMENTED'); + static const Code INTERNAL = Code._( + 13, + const $core.bool.fromEnvironment('protobuf.omit_enum_names') + ? '' + : 'INTERNAL'); + static const Code UNAVAILABLE = Code._( + 14, + const $core.bool.fromEnvironment('protobuf.omit_enum_names') + ? '' + : 'UNAVAILABLE'); + static const Code DATA_LOSS = Code._( + 15, + const $core.bool.fromEnvironment('protobuf.omit_enum_names') + ? '' + : 'DATA_LOSS'); + + static const $core.List<Code> values = <Code>[ + OK, + CANCELLED, + UNKNOWN, + INVALID_ARGUMENT, + DEADLINE_EXCEEDED, + NOT_FOUND, + ALREADY_EXISTS, + PERMISSION_DENIED, + UNAUTHENTICATED, + RESOURCE_EXHAUSTED, + FAILED_PRECONDITION, + ABORTED, + OUT_OF_RANGE, + UNIMPLEMENTED, + INTERNAL, + UNAVAILABLE, + DATA_LOSS, + ]; + + static final $core.Map<$core.int, Code> _byValue = + $pb.ProtobufEnum.initByValue(values); + static Code? valueOf($core.int value) => _byValue[value]; + + const Code._($core.int v, $core.String n) : super(v, n); +}
diff --git a/grpc/lib/src/generated/google/rpc/code.pbjson.dart b/grpc/lib/src/generated/google/rpc/code.pbjson.dart new file mode 100644 index 0000000..0c46b5f --- /dev/null +++ b/grpc/lib/src/generated/google/rpc/code.pbjson.dart
@@ -0,0 +1,31 @@ +/// +// Generated code. Do not modify. +// source: google/rpc/code.proto +// +// @dart = 2.12 +// ignore_for_file: annotate_overrides,camel_case_types,unnecessary_const,non_constant_identifier_names,library_prefixes,unused_import,unused_shown_name,return_of_invalid_type,unnecessary_this,prefer_final_fields + +import 'dart:core' as $core; + +const Code$json = const { + '1': 'Code', + '2': const [ + const {'1': 'OK', '2': 0}, + const {'1': 'CANCELLED', '2': 1}, + const {'1': 'UNKNOWN', '2': 2}, + const {'1': 'INVALID_ARGUMENT', '2': 3}, + const {'1': 'DEADLINE_EXCEEDED', '2': 4}, + const {'1': 'NOT_FOUND', '2': 5}, + const {'1': 'ALREADY_EXISTS', '2': 6}, + const {'1': 'PERMISSION_DENIED', '2': 7}, + const {'1': 'UNAUTHENTICATED', '2': 16}, + const {'1': 'RESOURCE_EXHAUSTED', '2': 8}, + const {'1': 'FAILED_PRECONDITION', '2': 9}, + const {'1': 'ABORTED', '2': 10}, + const {'1': 'OUT_OF_RANGE', '2': 11}, + const {'1': 'UNIMPLEMENTED', '2': 12}, + const {'1': 'INTERNAL', '2': 13}, + const {'1': 'UNAVAILABLE', '2': 14}, + const {'1': 'DATA_LOSS', '2': 15}, + ], +};
diff --git a/grpc/lib/src/generated/google/rpc/error_details.pb.dart b/grpc/lib/src/generated/google/rpc/error_details.pb.dart new file mode 100644 index 0000000..59dd91d --- /dev/null +++ b/grpc/lib/src/generated/google/rpc/error_details.pb.dart
@@ -0,0 +1,1185 @@ +/// +// Generated code. Do not modify. +// source: google/rpc/error_details.proto +// +// @dart = 2.12 +// ignore_for_file: annotate_overrides,camel_case_types,unnecessary_const,non_constant_identifier_names,library_prefixes,unused_import,unused_shown_name,return_of_invalid_type,unnecessary_this,prefer_final_fields + +import 'dart:core' as $core; + +import 'package:protobuf/protobuf.dart' as $pb; + +import '../protobuf/duration.pb.dart' as $1; + +class RetryInfo extends $pb.GeneratedMessage { + static final $pb.BuilderInfo _i = $pb.BuilderInfo( + const $core.bool.fromEnvironment('protobuf.omit_message_names') + ? '' + : 'RetryInfo', + package: const $pb.PackageName( + const $core.bool.fromEnvironment('protobuf.omit_message_names') + ? '' + : 'google.rpc'), + createEmptyInstance: create) + ..aOM<$1.Duration>( + 1, + const $core.bool.fromEnvironment('protobuf.omit_field_names') + ? '' + : 'retryDelay', + subBuilder: $1.Duration.create) + ..hasRequiredFields = false; + + RetryInfo._() : super(); + factory RetryInfo({ + $1.Duration? retryDelay, + }) { + final _result = create(); + if (retryDelay != null) { + _result.retryDelay = retryDelay; + } + return _result; + } + factory RetryInfo.fromBuffer($core.List<$core.int> i, + [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromBuffer(i, r); + factory RetryInfo.fromJson($core.String i, + [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromJson(i, r); + @$core.Deprecated('Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.deepCopy] instead. ' + 'Will be removed in next major version') + RetryInfo clone() => RetryInfo()..mergeFromMessage(this); + @$core.Deprecated('Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.rebuild] instead. ' + 'Will be removed in next major version') + RetryInfo copyWith(void Function(RetryInfo) updates) => + super.copyWith((message) => updates(message as RetryInfo)) + as RetryInfo; // ignore: deprecated_member_use + $pb.BuilderInfo get info_ => _i; + @$core.pragma('dart2js:noInline') + static RetryInfo create() => RetryInfo._(); + RetryInfo createEmptyInstance() => create(); + static $pb.PbList<RetryInfo> createRepeated() => $pb.PbList<RetryInfo>(); + @$core.pragma('dart2js:noInline') + static RetryInfo getDefault() => + _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor<RetryInfo>(create); + static RetryInfo? _defaultInstance; + + @$pb.TagNumber(1) + $1.Duration get retryDelay => $_getN(0); + @$pb.TagNumber(1) + set retryDelay($1.Duration v) { + setField(1, v); + } + + @$pb.TagNumber(1) + $core.bool hasRetryDelay() => $_has(0); + @$pb.TagNumber(1) + void clearRetryDelay() => clearField(1); + @$pb.TagNumber(1) + $1.Duration ensureRetryDelay() => $_ensure(0); +} + +class DebugInfo extends $pb.GeneratedMessage { + static final $pb.BuilderInfo _i = $pb.BuilderInfo( + const $core.bool.fromEnvironment('protobuf.omit_message_names') + ? '' + : 'DebugInfo', + package: const $pb.PackageName( + const $core.bool.fromEnvironment('protobuf.omit_message_names') + ? '' + : 'google.rpc'), + createEmptyInstance: create) + ..pPS( + 1, + const $core.bool.fromEnvironment('protobuf.omit_field_names') + ? '' + : 'stackEntries') + ..aOS( + 2, + const $core.bool.fromEnvironment('protobuf.omit_field_names') + ? '' + : 'detail') + ..hasRequiredFields = false; + + DebugInfo._() : super(); + factory DebugInfo({ + $core.Iterable<$core.String>? stackEntries, + $core.String? detail, + }) { + final _result = create(); + if (stackEntries != null) { + _result.stackEntries.addAll(stackEntries); + } + if (detail != null) { + _result.detail = detail; + } + return _result; + } + factory DebugInfo.fromBuffer($core.List<$core.int> i, + [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromBuffer(i, r); + factory DebugInfo.fromJson($core.String i, + [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromJson(i, r); + @$core.Deprecated('Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.deepCopy] instead. ' + 'Will be removed in next major version') + DebugInfo clone() => DebugInfo()..mergeFromMessage(this); + @$core.Deprecated('Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.rebuild] instead. ' + 'Will be removed in next major version') + DebugInfo copyWith(void Function(DebugInfo) updates) => + super.copyWith((message) => updates(message as DebugInfo)) + as DebugInfo; // ignore: deprecated_member_use + $pb.BuilderInfo get info_ => _i; + @$core.pragma('dart2js:noInline') + static DebugInfo create() => DebugInfo._(); + DebugInfo createEmptyInstance() => create(); + static $pb.PbList<DebugInfo> createRepeated() => $pb.PbList<DebugInfo>(); + @$core.pragma('dart2js:noInline') + static DebugInfo getDefault() => + _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor<DebugInfo>(create); + static DebugInfo? _defaultInstance; + + @$pb.TagNumber(1) + $core.List<$core.String> get stackEntries => $_getList(0); + + @$pb.TagNumber(2) + $core.String get detail => $_getSZ(1); + @$pb.TagNumber(2) + set detail($core.String v) { + $_setString(1, v); + } + + @$pb.TagNumber(2) + $core.bool hasDetail() => $_has(1); + @$pb.TagNumber(2) + void clearDetail() => clearField(2); +} + +class QuotaFailure_Violation extends $pb.GeneratedMessage { + static final $pb.BuilderInfo _i = $pb.BuilderInfo( + const $core.bool.fromEnvironment('protobuf.omit_message_names') + ? '' + : 'QuotaFailure.Violation', + package: const $pb.PackageName( + const $core.bool.fromEnvironment('protobuf.omit_message_names') + ? '' + : 'google.rpc'), + createEmptyInstance: create) + ..aOS( + 1, + const $core.bool.fromEnvironment('protobuf.omit_field_names') + ? '' + : 'subject') + ..aOS( + 2, + const $core.bool.fromEnvironment('protobuf.omit_field_names') + ? '' + : 'description') + ..hasRequiredFields = false; + + QuotaFailure_Violation._() : super(); + factory QuotaFailure_Violation({ + $core.String? subject, + $core.String? description, + }) { + final _result = create(); + if (subject != null) { + _result.subject = subject; + } + if (description != null) { + _result.description = description; + } + return _result; + } + factory QuotaFailure_Violation.fromBuffer($core.List<$core.int> i, + [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromBuffer(i, r); + factory QuotaFailure_Violation.fromJson($core.String i, + [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromJson(i, r); + @$core.Deprecated('Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.deepCopy] instead. ' + 'Will be removed in next major version') + QuotaFailure_Violation clone() => + QuotaFailure_Violation()..mergeFromMessage(this); + @$core.Deprecated('Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.rebuild] instead. ' + 'Will be removed in next major version') + QuotaFailure_Violation copyWith( + void Function(QuotaFailure_Violation) updates) => + super.copyWith((message) => updates(message as QuotaFailure_Violation)) + as QuotaFailure_Violation; // ignore: deprecated_member_use + $pb.BuilderInfo get info_ => _i; + @$core.pragma('dart2js:noInline') + static QuotaFailure_Violation create() => QuotaFailure_Violation._(); + QuotaFailure_Violation createEmptyInstance() => create(); + static $pb.PbList<QuotaFailure_Violation> createRepeated() => + $pb.PbList<QuotaFailure_Violation>(); + @$core.pragma('dart2js:noInline') + static QuotaFailure_Violation getDefault() => _defaultInstance ??= + $pb.GeneratedMessage.$_defaultFor<QuotaFailure_Violation>(create); + static QuotaFailure_Violation? _defaultInstance; + + @$pb.TagNumber(1) + $core.String get subject => $_getSZ(0); + @$pb.TagNumber(1) + set subject($core.String v) { + $_setString(0, v); + } + + @$pb.TagNumber(1) + $core.bool hasSubject() => $_has(0); + @$pb.TagNumber(1) + void clearSubject() => clearField(1); + + @$pb.TagNumber(2) + $core.String get description => $_getSZ(1); + @$pb.TagNumber(2) + set description($core.String v) { + $_setString(1, v); + } + + @$pb.TagNumber(2) + $core.bool hasDescription() => $_has(1); + @$pb.TagNumber(2) + void clearDescription() => clearField(2); +} + +class QuotaFailure extends $pb.GeneratedMessage { + static final $pb.BuilderInfo _i = $pb.BuilderInfo( + const $core.bool.fromEnvironment('protobuf.omit_message_names') + ? '' + : 'QuotaFailure', + package: const $pb.PackageName( + const $core.bool.fromEnvironment('protobuf.omit_message_names') + ? '' + : 'google.rpc'), + createEmptyInstance: create) + ..pc<QuotaFailure_Violation>( + 1, + const $core.bool.fromEnvironment('protobuf.omit_field_names') + ? '' + : 'violations', + $pb.PbFieldType.PM, + subBuilder: QuotaFailure_Violation.create) + ..hasRequiredFields = false; + + QuotaFailure._() : super(); + factory QuotaFailure({ + $core.Iterable<QuotaFailure_Violation>? violations, + }) { + final _result = create(); + if (violations != null) { + _result.violations.addAll(violations); + } + return _result; + } + factory QuotaFailure.fromBuffer($core.List<$core.int> i, + [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromBuffer(i, r); + factory QuotaFailure.fromJson($core.String i, + [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromJson(i, r); + @$core.Deprecated('Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.deepCopy] instead. ' + 'Will be removed in next major version') + QuotaFailure clone() => QuotaFailure()..mergeFromMessage(this); + @$core.Deprecated('Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.rebuild] instead. ' + 'Will be removed in next major version') + QuotaFailure copyWith(void Function(QuotaFailure) updates) => + super.copyWith((message) => updates(message as QuotaFailure)) + as QuotaFailure; // ignore: deprecated_member_use + $pb.BuilderInfo get info_ => _i; + @$core.pragma('dart2js:noInline') + static QuotaFailure create() => QuotaFailure._(); + QuotaFailure createEmptyInstance() => create(); + static $pb.PbList<QuotaFailure> createRepeated() => + $pb.PbList<QuotaFailure>(); + @$core.pragma('dart2js:noInline') + static QuotaFailure getDefault() => _defaultInstance ??= + $pb.GeneratedMessage.$_defaultFor<QuotaFailure>(create); + static QuotaFailure? _defaultInstance; + + @$pb.TagNumber(1) + $core.List<QuotaFailure_Violation> get violations => $_getList(0); +} + +class ErrorInfo extends $pb.GeneratedMessage { + static final $pb.BuilderInfo _i = $pb.BuilderInfo( + const $core.bool.fromEnvironment('protobuf.omit_message_names') + ? '' + : 'ErrorInfo', + package: const $pb.PackageName( + const $core.bool.fromEnvironment('protobuf.omit_message_names') + ? '' + : 'google.rpc'), + createEmptyInstance: create) + ..aOS( + 1, + const $core.bool.fromEnvironment('protobuf.omit_field_names') + ? '' + : 'reason') + ..aOS( + 2, + const $core.bool.fromEnvironment('protobuf.omit_field_names') + ? '' + : 'domain') + ..m<$core.String, $core.String>( + 3, + const $core.bool.fromEnvironment('protobuf.omit_field_names') + ? '' + : 'metadata', + entryClassName: 'ErrorInfo.MetadataEntry', + keyFieldType: $pb.PbFieldType.OS, + valueFieldType: $pb.PbFieldType.OS, + packageName: const $pb.PackageName('google.rpc')) + ..hasRequiredFields = false; + + ErrorInfo._() : super(); + factory ErrorInfo({ + $core.String? reason, + $core.String? domain, + $core.Map<$core.String, $core.String>? metadata, + }) { + final _result = create(); + if (reason != null) { + _result.reason = reason; + } + if (domain != null) { + _result.domain = domain; + } + if (metadata != null) { + _result.metadata.addAll(metadata); + } + return _result; + } + factory ErrorInfo.fromBuffer($core.List<$core.int> i, + [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromBuffer(i, r); + factory ErrorInfo.fromJson($core.String i, + [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromJson(i, r); + @$core.Deprecated('Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.deepCopy] instead. ' + 'Will be removed in next major version') + ErrorInfo clone() => ErrorInfo()..mergeFromMessage(this); + @$core.Deprecated('Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.rebuild] instead. ' + 'Will be removed in next major version') + ErrorInfo copyWith(void Function(ErrorInfo) updates) => + super.copyWith((message) => updates(message as ErrorInfo)) + as ErrorInfo; // ignore: deprecated_member_use + $pb.BuilderInfo get info_ => _i; + @$core.pragma('dart2js:noInline') + static ErrorInfo create() => ErrorInfo._(); + ErrorInfo createEmptyInstance() => create(); + static $pb.PbList<ErrorInfo> createRepeated() => $pb.PbList<ErrorInfo>(); + @$core.pragma('dart2js:noInline') + static ErrorInfo getDefault() => + _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor<ErrorInfo>(create); + static ErrorInfo? _defaultInstance; + + @$pb.TagNumber(1) + $core.String get reason => $_getSZ(0); + @$pb.TagNumber(1) + set reason($core.String v) { + $_setString(0, v); + } + + @$pb.TagNumber(1) + $core.bool hasReason() => $_has(0); + @$pb.TagNumber(1) + void clearReason() => clearField(1); + + @$pb.TagNumber(2) + $core.String get domain => $_getSZ(1); + @$pb.TagNumber(2) + set domain($core.String v) { + $_setString(1, v); + } + + @$pb.TagNumber(2) + $core.bool hasDomain() => $_has(1); + @$pb.TagNumber(2) + void clearDomain() => clearField(2); + + @$pb.TagNumber(3) + $core.Map<$core.String, $core.String> get metadata => $_getMap(2); +} + +class PreconditionFailure_Violation extends $pb.GeneratedMessage { + static final $pb.BuilderInfo _i = $pb.BuilderInfo( + const $core.bool.fromEnvironment('protobuf.omit_message_names') + ? '' + : 'PreconditionFailure.Violation', + package: const $pb.PackageName( + const $core.bool.fromEnvironment('protobuf.omit_message_names') + ? '' + : 'google.rpc'), + createEmptyInstance: create) + ..aOS( + 1, + const $core.bool.fromEnvironment('protobuf.omit_field_names') + ? '' + : 'type') + ..aOS( + 2, + const $core.bool.fromEnvironment('protobuf.omit_field_names') + ? '' + : 'subject') + ..aOS( + 3, + const $core.bool.fromEnvironment('protobuf.omit_field_names') + ? '' + : 'description') + ..hasRequiredFields = false; + + PreconditionFailure_Violation._() : super(); + factory PreconditionFailure_Violation({ + $core.String? type, + $core.String? subject, + $core.String? description, + }) { + final _result = create(); + if (type != null) { + _result.type = type; + } + if (subject != null) { + _result.subject = subject; + } + if (description != null) { + _result.description = description; + } + return _result; + } + factory PreconditionFailure_Violation.fromBuffer($core.List<$core.int> i, + [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromBuffer(i, r); + factory PreconditionFailure_Violation.fromJson($core.String i, + [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromJson(i, r); + @$core.Deprecated('Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.deepCopy] instead. ' + 'Will be removed in next major version') + PreconditionFailure_Violation clone() => + PreconditionFailure_Violation()..mergeFromMessage(this); + @$core.Deprecated('Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.rebuild] instead. ' + 'Will be removed in next major version') + PreconditionFailure_Violation copyWith( + void Function(PreconditionFailure_Violation) updates) => + super.copyWith( + (message) => updates(message as PreconditionFailure_Violation)) + as PreconditionFailure_Violation; // ignore: deprecated_member_use + $pb.BuilderInfo get info_ => _i; + @$core.pragma('dart2js:noInline') + static PreconditionFailure_Violation create() => + PreconditionFailure_Violation._(); + PreconditionFailure_Violation createEmptyInstance() => create(); + static $pb.PbList<PreconditionFailure_Violation> createRepeated() => + $pb.PbList<PreconditionFailure_Violation>(); + @$core.pragma('dart2js:noInline') + static PreconditionFailure_Violation getDefault() => _defaultInstance ??= + $pb.GeneratedMessage.$_defaultFor<PreconditionFailure_Violation>(create); + static PreconditionFailure_Violation? _defaultInstance; + + @$pb.TagNumber(1) + $core.String get type => $_getSZ(0); + @$pb.TagNumber(1) + set type($core.String v) { + $_setString(0, v); + } + + @$pb.TagNumber(1) + $core.bool hasType() => $_has(0); + @$pb.TagNumber(1) + void clearType() => clearField(1); + + @$pb.TagNumber(2) + $core.String get subject => $_getSZ(1); + @$pb.TagNumber(2) + set subject($core.String v) { + $_setString(1, v); + } + + @$pb.TagNumber(2) + $core.bool hasSubject() => $_has(1); + @$pb.TagNumber(2) + void clearSubject() => clearField(2); + + @$pb.TagNumber(3) + $core.String get description => $_getSZ(2); + @$pb.TagNumber(3) + set description($core.String v) { + $_setString(2, v); + } + + @$pb.TagNumber(3) + $core.bool hasDescription() => $_has(2); + @$pb.TagNumber(3) + void clearDescription() => clearField(3); +} + +class PreconditionFailure extends $pb.GeneratedMessage { + static final $pb.BuilderInfo _i = $pb.BuilderInfo( + const $core.bool.fromEnvironment('protobuf.omit_message_names') + ? '' + : 'PreconditionFailure', + package: const $pb.PackageName( + const $core.bool.fromEnvironment('protobuf.omit_message_names') + ? '' + : 'google.rpc'), + createEmptyInstance: create) + ..pc<PreconditionFailure_Violation>( + 1, + const $core.bool.fromEnvironment('protobuf.omit_field_names') + ? '' + : 'violations', + $pb.PbFieldType.PM, + subBuilder: PreconditionFailure_Violation.create) + ..hasRequiredFields = false; + + PreconditionFailure._() : super(); + factory PreconditionFailure({ + $core.Iterable<PreconditionFailure_Violation>? violations, + }) { + final _result = create(); + if (violations != null) { + _result.violations.addAll(violations); + } + return _result; + } + factory PreconditionFailure.fromBuffer($core.List<$core.int> i, + [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromBuffer(i, r); + factory PreconditionFailure.fromJson($core.String i, + [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromJson(i, r); + @$core.Deprecated('Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.deepCopy] instead. ' + 'Will be removed in next major version') + PreconditionFailure clone() => PreconditionFailure()..mergeFromMessage(this); + @$core.Deprecated('Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.rebuild] instead. ' + 'Will be removed in next major version') + PreconditionFailure copyWith(void Function(PreconditionFailure) updates) => + super.copyWith((message) => updates(message as PreconditionFailure)) + as PreconditionFailure; // ignore: deprecated_member_use + $pb.BuilderInfo get info_ => _i; + @$core.pragma('dart2js:noInline') + static PreconditionFailure create() => PreconditionFailure._(); + PreconditionFailure createEmptyInstance() => create(); + static $pb.PbList<PreconditionFailure> createRepeated() => + $pb.PbList<PreconditionFailure>(); + @$core.pragma('dart2js:noInline') + static PreconditionFailure getDefault() => _defaultInstance ??= + $pb.GeneratedMessage.$_defaultFor<PreconditionFailure>(create); + static PreconditionFailure? _defaultInstance; + + @$pb.TagNumber(1) + $core.List<PreconditionFailure_Violation> get violations => $_getList(0); +} + +class BadRequest_FieldViolation extends $pb.GeneratedMessage { + static final $pb.BuilderInfo _i = $pb.BuilderInfo( + const $core.bool.fromEnvironment('protobuf.omit_message_names') + ? '' + : 'BadRequest.FieldViolation', + package: const $pb.PackageName( + const $core.bool.fromEnvironment('protobuf.omit_message_names') + ? '' + : 'google.rpc'), + createEmptyInstance: create) + ..aOS( + 1, + const $core.bool.fromEnvironment('protobuf.omit_field_names') + ? '' + : 'field') + ..aOS( + 2, + const $core.bool.fromEnvironment('protobuf.omit_field_names') + ? '' + : 'description') + ..hasRequiredFields = false; + + BadRequest_FieldViolation._() : super(); + factory BadRequest_FieldViolation({ + $core.String? field_1, + $core.String? description, + }) { + final _result = create(); + if (field_1 != null) { + _result.field_1 = field_1; + } + if (description != null) { + _result.description = description; + } + return _result; + } + factory BadRequest_FieldViolation.fromBuffer($core.List<$core.int> i, + [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromBuffer(i, r); + factory BadRequest_FieldViolation.fromJson($core.String i, + [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromJson(i, r); + @$core.Deprecated('Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.deepCopy] instead. ' + 'Will be removed in next major version') + BadRequest_FieldViolation clone() => + BadRequest_FieldViolation()..mergeFromMessage(this); + @$core.Deprecated('Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.rebuild] instead. ' + 'Will be removed in next major version') + BadRequest_FieldViolation copyWith( + void Function(BadRequest_FieldViolation) updates) => + super.copyWith((message) => updates(message as BadRequest_FieldViolation)) + as BadRequest_FieldViolation; // ignore: deprecated_member_use + $pb.BuilderInfo get info_ => _i; + @$core.pragma('dart2js:noInline') + static BadRequest_FieldViolation create() => BadRequest_FieldViolation._(); + BadRequest_FieldViolation createEmptyInstance() => create(); + static $pb.PbList<BadRequest_FieldViolation> createRepeated() => + $pb.PbList<BadRequest_FieldViolation>(); + @$core.pragma('dart2js:noInline') + static BadRequest_FieldViolation getDefault() => _defaultInstance ??= + $pb.GeneratedMessage.$_defaultFor<BadRequest_FieldViolation>(create); + static BadRequest_FieldViolation? _defaultInstance; + + @$pb.TagNumber(1) + $core.String get field_1 => $_getSZ(0); + @$pb.TagNumber(1) + set field_1($core.String v) { + $_setString(0, v); + } + + @$pb.TagNumber(1) + $core.bool hasField_1() => $_has(0); + @$pb.TagNumber(1) + void clearField_1() => clearField(1); + + @$pb.TagNumber(2) + $core.String get description => $_getSZ(1); + @$pb.TagNumber(2) + set description($core.String v) { + $_setString(1, v); + } + + @$pb.TagNumber(2) + $core.bool hasDescription() => $_has(1); + @$pb.TagNumber(2) + void clearDescription() => clearField(2); +} + +class BadRequest extends $pb.GeneratedMessage { + static final $pb.BuilderInfo _i = $pb.BuilderInfo( + const $core.bool.fromEnvironment('protobuf.omit_message_names') + ? '' + : 'BadRequest', + package: const $pb.PackageName( + const $core.bool.fromEnvironment('protobuf.omit_message_names') + ? '' + : 'google.rpc'), + createEmptyInstance: create) + ..pc<BadRequest_FieldViolation>( + 1, + const $core.bool.fromEnvironment('protobuf.omit_field_names') + ? '' + : 'fieldViolations', + $pb.PbFieldType.PM, + subBuilder: BadRequest_FieldViolation.create) + ..hasRequiredFields = false; + + BadRequest._() : super(); + factory BadRequest({ + $core.Iterable<BadRequest_FieldViolation>? fieldViolations, + }) { + final _result = create(); + if (fieldViolations != null) { + _result.fieldViolations.addAll(fieldViolations); + } + return _result; + } + factory BadRequest.fromBuffer($core.List<$core.int> i, + [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromBuffer(i, r); + factory BadRequest.fromJson($core.String i, + [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromJson(i, r); + @$core.Deprecated('Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.deepCopy] instead. ' + 'Will be removed in next major version') + BadRequest clone() => BadRequest()..mergeFromMessage(this); + @$core.Deprecated('Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.rebuild] instead. ' + 'Will be removed in next major version') + BadRequest copyWith(void Function(BadRequest) updates) => + super.copyWith((message) => updates(message as BadRequest)) + as BadRequest; // ignore: deprecated_member_use + $pb.BuilderInfo get info_ => _i; + @$core.pragma('dart2js:noInline') + static BadRequest create() => BadRequest._(); + BadRequest createEmptyInstance() => create(); + static $pb.PbList<BadRequest> createRepeated() => $pb.PbList<BadRequest>(); + @$core.pragma('dart2js:noInline') + static BadRequest getDefault() => _defaultInstance ??= + $pb.GeneratedMessage.$_defaultFor<BadRequest>(create); + static BadRequest? _defaultInstance; + + @$pb.TagNumber(1) + $core.List<BadRequest_FieldViolation> get fieldViolations => $_getList(0); +} + +class RequestInfo extends $pb.GeneratedMessage { + static final $pb.BuilderInfo _i = $pb.BuilderInfo( + const $core.bool.fromEnvironment('protobuf.omit_message_names') + ? '' + : 'RequestInfo', + package: const $pb.PackageName( + const $core.bool.fromEnvironment('protobuf.omit_message_names') + ? '' + : 'google.rpc'), + createEmptyInstance: create) + ..aOS( + 1, + const $core.bool.fromEnvironment('protobuf.omit_field_names') + ? '' + : 'requestId') + ..aOS( + 2, + const $core.bool.fromEnvironment('protobuf.omit_field_names') + ? '' + : 'servingData') + ..hasRequiredFields = false; + + RequestInfo._() : super(); + factory RequestInfo({ + $core.String? requestId, + $core.String? servingData, + }) { + final _result = create(); + if (requestId != null) { + _result.requestId = requestId; + } + if (servingData != null) { + _result.servingData = servingData; + } + return _result; + } + factory RequestInfo.fromBuffer($core.List<$core.int> i, + [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromBuffer(i, r); + factory RequestInfo.fromJson($core.String i, + [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromJson(i, r); + @$core.Deprecated('Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.deepCopy] instead. ' + 'Will be removed in next major version') + RequestInfo clone() => RequestInfo()..mergeFromMessage(this); + @$core.Deprecated('Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.rebuild] instead. ' + 'Will be removed in next major version') + RequestInfo copyWith(void Function(RequestInfo) updates) => + super.copyWith((message) => updates(message as RequestInfo)) + as RequestInfo; // ignore: deprecated_member_use + $pb.BuilderInfo get info_ => _i; + @$core.pragma('dart2js:noInline') + static RequestInfo create() => RequestInfo._(); + RequestInfo createEmptyInstance() => create(); + static $pb.PbList<RequestInfo> createRepeated() => $pb.PbList<RequestInfo>(); + @$core.pragma('dart2js:noInline') + static RequestInfo getDefault() => _defaultInstance ??= + $pb.GeneratedMessage.$_defaultFor<RequestInfo>(create); + static RequestInfo? _defaultInstance; + + @$pb.TagNumber(1) + $core.String get requestId => $_getSZ(0); + @$pb.TagNumber(1) + set requestId($core.String v) { + $_setString(0, v); + } + + @$pb.TagNumber(1) + $core.bool hasRequestId() => $_has(0); + @$pb.TagNumber(1) + void clearRequestId() => clearField(1); + + @$pb.TagNumber(2) + $core.String get servingData => $_getSZ(1); + @$pb.TagNumber(2) + set servingData($core.String v) { + $_setString(1, v); + } + + @$pb.TagNumber(2) + $core.bool hasServingData() => $_has(1); + @$pb.TagNumber(2) + void clearServingData() => clearField(2); +} + +class ResourceInfo extends $pb.GeneratedMessage { + static final $pb.BuilderInfo _i = $pb.BuilderInfo( + const $core.bool.fromEnvironment('protobuf.omit_message_names') + ? '' + : 'ResourceInfo', + package: const $pb.PackageName( + const $core.bool.fromEnvironment('protobuf.omit_message_names') + ? '' + : 'google.rpc'), + createEmptyInstance: create) + ..aOS( + 1, + const $core.bool.fromEnvironment('protobuf.omit_field_names') + ? '' + : 'resourceType') + ..aOS( + 2, + const $core.bool.fromEnvironment('protobuf.omit_field_names') + ? '' + : 'resourceName') + ..aOS( + 3, + const $core.bool.fromEnvironment('protobuf.omit_field_names') + ? '' + : 'owner') + ..aOS( + 4, + const $core.bool.fromEnvironment('protobuf.omit_field_names') + ? '' + : 'description') + ..hasRequiredFields = false; + + ResourceInfo._() : super(); + factory ResourceInfo({ + $core.String? resourceType, + $core.String? resourceName, + $core.String? owner, + $core.String? description, + }) { + final _result = create(); + if (resourceType != null) { + _result.resourceType = resourceType; + } + if (resourceName != null) { + _result.resourceName = resourceName; + } + if (owner != null) { + _result.owner = owner; + } + if (description != null) { + _result.description = description; + } + return _result; + } + factory ResourceInfo.fromBuffer($core.List<$core.int> i, + [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromBuffer(i, r); + factory ResourceInfo.fromJson($core.String i, + [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromJson(i, r); + @$core.Deprecated('Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.deepCopy] instead. ' + 'Will be removed in next major version') + ResourceInfo clone() => ResourceInfo()..mergeFromMessage(this); + @$core.Deprecated('Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.rebuild] instead. ' + 'Will be removed in next major version') + ResourceInfo copyWith(void Function(ResourceInfo) updates) => + super.copyWith((message) => updates(message as ResourceInfo)) + as ResourceInfo; // ignore: deprecated_member_use + $pb.BuilderInfo get info_ => _i; + @$core.pragma('dart2js:noInline') + static ResourceInfo create() => ResourceInfo._(); + ResourceInfo createEmptyInstance() => create(); + static $pb.PbList<ResourceInfo> createRepeated() => + $pb.PbList<ResourceInfo>(); + @$core.pragma('dart2js:noInline') + static ResourceInfo getDefault() => _defaultInstance ??= + $pb.GeneratedMessage.$_defaultFor<ResourceInfo>(create); + static ResourceInfo? _defaultInstance; + + @$pb.TagNumber(1) + $core.String get resourceType => $_getSZ(0); + @$pb.TagNumber(1) + set resourceType($core.String v) { + $_setString(0, v); + } + + @$pb.TagNumber(1) + $core.bool hasResourceType() => $_has(0); + @$pb.TagNumber(1) + void clearResourceType() => clearField(1); + + @$pb.TagNumber(2) + $core.String get resourceName => $_getSZ(1); + @$pb.TagNumber(2) + set resourceName($core.String v) { + $_setString(1, v); + } + + @$pb.TagNumber(2) + $core.bool hasResourceName() => $_has(1); + @$pb.TagNumber(2) + void clearResourceName() => clearField(2); + + @$pb.TagNumber(3) + $core.String get owner => $_getSZ(2); + @$pb.TagNumber(3) + set owner($core.String v) { + $_setString(2, v); + } + + @$pb.TagNumber(3) + $core.bool hasOwner() => $_has(2); + @$pb.TagNumber(3) + void clearOwner() => clearField(3); + + @$pb.TagNumber(4) + $core.String get description => $_getSZ(3); + @$pb.TagNumber(4) + set description($core.String v) { + $_setString(3, v); + } + + @$pb.TagNumber(4) + $core.bool hasDescription() => $_has(3); + @$pb.TagNumber(4) + void clearDescription() => clearField(4); +} + +class Help_Link extends $pb.GeneratedMessage { + static final $pb.BuilderInfo _i = $pb.BuilderInfo( + const $core.bool.fromEnvironment('protobuf.omit_message_names') + ? '' + : 'Help.Link', + package: const $pb.PackageName( + const $core.bool.fromEnvironment('protobuf.omit_message_names') + ? '' + : 'google.rpc'), + createEmptyInstance: create) + ..aOS( + 1, + const $core.bool.fromEnvironment('protobuf.omit_field_names') + ? '' + : 'description') + ..aOS( + 2, + const $core.bool.fromEnvironment('protobuf.omit_field_names') + ? '' + : 'url') + ..hasRequiredFields = false; + + Help_Link._() : super(); + factory Help_Link({ + $core.String? description, + $core.String? url, + }) { + final _result = create(); + if (description != null) { + _result.description = description; + } + if (url != null) { + _result.url = url; + } + return _result; + } + factory Help_Link.fromBuffer($core.List<$core.int> i, + [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromBuffer(i, r); + factory Help_Link.fromJson($core.String i, + [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromJson(i, r); + @$core.Deprecated('Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.deepCopy] instead. ' + 'Will be removed in next major version') + Help_Link clone() => Help_Link()..mergeFromMessage(this); + @$core.Deprecated('Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.rebuild] instead. ' + 'Will be removed in next major version') + Help_Link copyWith(void Function(Help_Link) updates) => + super.copyWith((message) => updates(message as Help_Link)) + as Help_Link; // ignore: deprecated_member_use + $pb.BuilderInfo get info_ => _i; + @$core.pragma('dart2js:noInline') + static Help_Link create() => Help_Link._(); + Help_Link createEmptyInstance() => create(); + static $pb.PbList<Help_Link> createRepeated() => $pb.PbList<Help_Link>(); + @$core.pragma('dart2js:noInline') + static Help_Link getDefault() => + _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor<Help_Link>(create); + static Help_Link? _defaultInstance; + + @$pb.TagNumber(1) + $core.String get description => $_getSZ(0); + @$pb.TagNumber(1) + set description($core.String v) { + $_setString(0, v); + } + + @$pb.TagNumber(1) + $core.bool hasDescription() => $_has(0); + @$pb.TagNumber(1) + void clearDescription() => clearField(1); + + @$pb.TagNumber(2) + $core.String get url => $_getSZ(1); + @$pb.TagNumber(2) + set url($core.String v) { + $_setString(1, v); + } + + @$pb.TagNumber(2) + $core.bool hasUrl() => $_has(1); + @$pb.TagNumber(2) + void clearUrl() => clearField(2); +} + +class Help extends $pb.GeneratedMessage { + static final $pb.BuilderInfo _i = $pb.BuilderInfo( + const $core.bool.fromEnvironment('protobuf.omit_message_names') + ? '' + : 'Help', + package: const $pb.PackageName( + const $core.bool.fromEnvironment('protobuf.omit_message_names') + ? '' + : 'google.rpc'), + createEmptyInstance: create) + ..pc<Help_Link>( + 1, + const $core.bool.fromEnvironment('protobuf.omit_field_names') + ? '' + : 'links', + $pb.PbFieldType.PM, + subBuilder: Help_Link.create) + ..hasRequiredFields = false; + + Help._() : super(); + factory Help({ + $core.Iterable<Help_Link>? links, + }) { + final _result = create(); + if (links != null) { + _result.links.addAll(links); + } + return _result; + } + factory Help.fromBuffer($core.List<$core.int> i, + [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromBuffer(i, r); + factory Help.fromJson($core.String i, + [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromJson(i, r); + @$core.Deprecated('Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.deepCopy] instead. ' + 'Will be removed in next major version') + Help clone() => Help()..mergeFromMessage(this); + @$core.Deprecated('Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.rebuild] instead. ' + 'Will be removed in next major version') + Help copyWith(void Function(Help) updates) => + super.copyWith((message) => updates(message as Help)) + as Help; // ignore: deprecated_member_use + $pb.BuilderInfo get info_ => _i; + @$core.pragma('dart2js:noInline') + static Help create() => Help._(); + Help createEmptyInstance() => create(); + static $pb.PbList<Help> createRepeated() => $pb.PbList<Help>(); + @$core.pragma('dart2js:noInline') + static Help getDefault() => + _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor<Help>(create); + static Help? _defaultInstance; + + @$pb.TagNumber(1) + $core.List<Help_Link> get links => $_getList(0); +} + +class LocalizedMessage extends $pb.GeneratedMessage { + static final $pb.BuilderInfo _i = $pb.BuilderInfo( + const $core.bool.fromEnvironment('protobuf.omit_message_names') + ? '' + : 'LocalizedMessage', + package: const $pb.PackageName( + const $core.bool.fromEnvironment('protobuf.omit_message_names') + ? '' + : 'google.rpc'), + createEmptyInstance: create) + ..aOS( + 1, + const $core.bool.fromEnvironment('protobuf.omit_field_names') + ? '' + : 'locale') + ..aOS( + 2, + const $core.bool.fromEnvironment('protobuf.omit_field_names') + ? '' + : 'message') + ..hasRequiredFields = false; + + LocalizedMessage._() : super(); + factory LocalizedMessage({ + $core.String? locale, + $core.String? message, + }) { + final _result = create(); + if (locale != null) { + _result.locale = locale; + } + if (message != null) { + _result.message = message; + } + return _result; + } + factory LocalizedMessage.fromBuffer($core.List<$core.int> i, + [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromBuffer(i, r); + factory LocalizedMessage.fromJson($core.String i, + [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromJson(i, r); + @$core.Deprecated('Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.deepCopy] instead. ' + 'Will be removed in next major version') + LocalizedMessage clone() => LocalizedMessage()..mergeFromMessage(this); + @$core.Deprecated('Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.rebuild] instead. ' + 'Will be removed in next major version') + LocalizedMessage copyWith(void Function(LocalizedMessage) updates) => + super.copyWith((message) => updates(message as LocalizedMessage)) + as LocalizedMessage; // ignore: deprecated_member_use + $pb.BuilderInfo get info_ => _i; + @$core.pragma('dart2js:noInline') + static LocalizedMessage create() => LocalizedMessage._(); + LocalizedMessage createEmptyInstance() => create(); + static $pb.PbList<LocalizedMessage> createRepeated() => + $pb.PbList<LocalizedMessage>(); + @$core.pragma('dart2js:noInline') + static LocalizedMessage getDefault() => _defaultInstance ??= + $pb.GeneratedMessage.$_defaultFor<LocalizedMessage>(create); + static LocalizedMessage? _defaultInstance; + + @$pb.TagNumber(1) + $core.String get locale => $_getSZ(0); + @$pb.TagNumber(1) + set locale($core.String v) { + $_setString(0, v); + } + + @$pb.TagNumber(1) + $core.bool hasLocale() => $_has(0); + @$pb.TagNumber(1) + void clearLocale() => clearField(1); + + @$pb.TagNumber(2) + $core.String get message => $_getSZ(1); + @$pb.TagNumber(2) + set message($core.String v) { + $_setString(1, v); + } + + @$pb.TagNumber(2) + $core.bool hasMessage() => $_has(1); + @$pb.TagNumber(2) + void clearMessage() => clearField(2); +}
diff --git a/grpc/lib/src/generated/google/rpc/error_details.pbenum.dart b/grpc/lib/src/generated/google/rpc/error_details.pbenum.dart new file mode 100644 index 0000000..dc40603 --- /dev/null +++ b/grpc/lib/src/generated/google/rpc/error_details.pbenum.dart
@@ -0,0 +1,6 @@ +/// +// Generated code. Do not modify. +// source: google/rpc/error_details.proto +// +// @dart = 2.12 +// ignore_for_file: annotate_overrides,camel_case_types,unnecessary_const,non_constant_identifier_names,library_prefixes,unused_import,unused_shown_name,return_of_invalid_type,unnecessary_this,prefer_final_fields
diff --git a/grpc/lib/src/generated/google/rpc/error_details.pbjson.dart b/grpc/lib/src/generated/google/rpc/error_details.pbjson.dart new file mode 100644 index 0000000..d1071f9 --- /dev/null +++ b/grpc/lib/src/generated/google/rpc/error_details.pbjson.dart
@@ -0,0 +1,175 @@ +/// +// Generated code. Do not modify. +// source: google/rpc/error_details.proto +// +// @dart = 2.12 +// ignore_for_file: annotate_overrides,camel_case_types,unnecessary_const,non_constant_identifier_names,library_prefixes,unused_import,unused_shown_name,return_of_invalid_type,unnecessary_this,prefer_final_fields + +import 'dart:core' as $core; + +const RetryInfo$json = const { + '1': 'RetryInfo', + '2': const [ + const { + '1': 'retry_delay', + '3': 1, + '4': 1, + '5': 11, + '6': '.google.protobuf.Duration', + '10': 'retryDelay' + }, + ], +}; + +const DebugInfo$json = const { + '1': 'DebugInfo', + '2': const [ + const {'1': 'stack_entries', '3': 1, '4': 3, '5': 9, '10': 'stackEntries'}, + const {'1': 'detail', '3': 2, '4': 1, '5': 9, '10': 'detail'}, + ], +}; + +const QuotaFailure$json = const { + '1': 'QuotaFailure', + '2': const [ + const { + '1': 'violations', + '3': 1, + '4': 3, + '5': 11, + '6': '.google.rpc.QuotaFailure.Violation', + '10': 'violations' + }, + ], + '3': const [QuotaFailure_Violation$json], +}; + +const QuotaFailure_Violation$json = const { + '1': 'Violation', + '2': const [ + const {'1': 'subject', '3': 1, '4': 1, '5': 9, '10': 'subject'}, + const {'1': 'description', '3': 2, '4': 1, '5': 9, '10': 'description'}, + ], +}; + +const ErrorInfo$json = const { + '1': 'ErrorInfo', + '2': const [ + const {'1': 'reason', '3': 1, '4': 1, '5': 9, '10': 'reason'}, + const {'1': 'domain', '3': 2, '4': 1, '5': 9, '10': 'domain'}, + const { + '1': 'metadata', + '3': 3, + '4': 3, + '5': 11, + '6': '.google.rpc.ErrorInfo.MetadataEntry', + '10': 'metadata' + }, + ], + '3': const [ErrorInfo_MetadataEntry$json], +}; + +const ErrorInfo_MetadataEntry$json = const { + '1': 'MetadataEntry', + '2': const [ + const {'1': 'key', '3': 1, '4': 1, '5': 9, '10': 'key'}, + const {'1': 'value', '3': 2, '4': 1, '5': 9, '10': 'value'}, + ], + '7': const {'7': true}, +}; + +const PreconditionFailure$json = const { + '1': 'PreconditionFailure', + '2': const [ + const { + '1': 'violations', + '3': 1, + '4': 3, + '5': 11, + '6': '.google.rpc.PreconditionFailure.Violation', + '10': 'violations' + }, + ], + '3': const [PreconditionFailure_Violation$json], +}; + +const PreconditionFailure_Violation$json = const { + '1': 'Violation', + '2': const [ + const {'1': 'type', '3': 1, '4': 1, '5': 9, '10': 'type'}, + const {'1': 'subject', '3': 2, '4': 1, '5': 9, '10': 'subject'}, + const {'1': 'description', '3': 3, '4': 1, '5': 9, '10': 'description'}, + ], +}; + +const BadRequest$json = const { + '1': 'BadRequest', + '2': const [ + const { + '1': 'field_violations', + '3': 1, + '4': 3, + '5': 11, + '6': '.google.rpc.BadRequest.FieldViolation', + '10': 'fieldViolations' + }, + ], + '3': const [BadRequest_FieldViolation$json], +}; + +const BadRequest_FieldViolation$json = const { + '1': 'FieldViolation', + '2': const [ + const {'1': 'field', '3': 1, '4': 1, '5': 9, '10': 'field'}, + const {'1': 'description', '3': 2, '4': 1, '5': 9, '10': 'description'}, + ], +}; + +const RequestInfo$json = const { + '1': 'RequestInfo', + '2': const [ + const {'1': 'request_id', '3': 1, '4': 1, '5': 9, '10': 'requestId'}, + const {'1': 'serving_data', '3': 2, '4': 1, '5': 9, '10': 'servingData'}, + ], +}; + +const ResourceInfo$json = const { + '1': 'ResourceInfo', + '2': const [ + const {'1': 'resource_type', '3': 1, '4': 1, '5': 9, '10': 'resourceType'}, + const {'1': 'resource_name', '3': 2, '4': 1, '5': 9, '10': 'resourceName'}, + const {'1': 'owner', '3': 3, '4': 1, '5': 9, '10': 'owner'}, + const {'1': 'description', '3': 4, '4': 1, '5': 9, '10': 'description'}, + ], +}; + +const Help$json = const { + '1': 'Help', + '2': const [ + const { + '1': 'links', + '3': 1, + '4': 3, + '5': 11, + '6': '.google.rpc.Help.Link', + '10': 'links' + }, + ], + '3': const [Help_Link$json], +}; + +const Help_Link$json = const { + '1': 'Link', + '2': const [ + const {'1': 'description', '3': 1, '4': 1, '5': 9, '10': 'description'}, + const {'1': 'url', '3': 2, '4': 1, '5': 9, '10': 'url'}, + ], +}; + +const LocalizedMessage$json = const { + '1': 'LocalizedMessage', + '2': const [ + const {'1': 'locale', '3': 1, '4': 1, '5': 9, '10': 'locale'}, + const {'1': 'message', '3': 2, '4': 1, '5': 9, '10': 'message'}, + ], +};
diff --git a/grpc/lib/src/generated/google/rpc/status.pb.dart b/grpc/lib/src/generated/google/rpc/status.pb.dart new file mode 100644 index 0000000..718cc9c --- /dev/null +++ b/grpc/lib/src/generated/google/rpc/status.pb.dart
@@ -0,0 +1,114 @@ +/// +// Generated code. Do not modify. +// source: google/rpc/status.proto +// +// @dart = 2.12 +// ignore_for_file: annotate_overrides,camel_case_types,unnecessary_const,non_constant_identifier_names,library_prefixes,unused_import,unused_shown_name,return_of_invalid_type,unnecessary_this,prefer_final_fields + +import 'dart:core' as $core; + +import 'package:protobuf/protobuf.dart' as $pb; + +import '../protobuf/any.pb.dart' as $0; + +class Status extends $pb.GeneratedMessage { + static final $pb.BuilderInfo _i = $pb.BuilderInfo( + const $core.bool.fromEnvironment('protobuf.omit_message_names') + ? '' + : 'Status', + package: const $pb.PackageName( + const $core.bool.fromEnvironment('protobuf.omit_message_names') + ? '' + : 'google.rpc'), + createEmptyInstance: create) + ..a<$core.int>( + 1, + const $core.bool.fromEnvironment('protobuf.omit_field_names') + ? '' + : 'code', + $pb.PbFieldType.O3) + ..aOS( + 2, + const $core.bool.fromEnvironment('protobuf.omit_field_names') + ? '' + : 'message') + ..pc<$0.Any>( + 3, + const $core.bool.fromEnvironment('protobuf.omit_field_names') + ? '' + : 'details', + $pb.PbFieldType.PM, + subBuilder: $0.Any.create) + ..hasRequiredFields = false; + + Status._() : super(); + factory Status({ + $core.int? code, + $core.String? message, + $core.Iterable<$0.Any>? details, + }) { + final _result = create(); + if (code != null) { + _result.code = code; + } + if (message != null) { + _result.message = message; + } + if (details != null) { + _result.details.addAll(details); + } + return _result; + } + factory Status.fromBuffer($core.List<$core.int> i, + [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromBuffer(i, r); + factory Status.fromJson($core.String i, + [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromJson(i, r); + @$core.Deprecated('Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.deepCopy] instead. ' + 'Will be removed in next major version') + Status clone() => Status()..mergeFromMessage(this); + @$core.Deprecated('Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.rebuild] instead. ' + 'Will be removed in next major version') + Status copyWith(void Function(Status) updates) => + super.copyWith((message) => updates(message as Status)) + as Status; // ignore: deprecated_member_use + $pb.BuilderInfo get info_ => _i; + @$core.pragma('dart2js:noInline') + static Status create() => Status._(); + Status createEmptyInstance() => create(); + static $pb.PbList<Status> createRepeated() => $pb.PbList<Status>(); + @$core.pragma('dart2js:noInline') + static Status getDefault() => + _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor<Status>(create); + static Status? _defaultInstance; + + @$pb.TagNumber(1) + $core.int get code => $_getIZ(0); + @$pb.TagNumber(1) + set code($core.int v) { + $_setSignedInt32(0, v); + } + + @$pb.TagNumber(1) + $core.bool hasCode() => $_has(0); + @$pb.TagNumber(1) + void clearCode() => clearField(1); + + @$pb.TagNumber(2) + $core.String get message => $_getSZ(1); + @$pb.TagNumber(2) + set message($core.String v) { + $_setString(1, v); + } + + @$pb.TagNumber(2) + $core.bool hasMessage() => $_has(1); + @$pb.TagNumber(2) + void clearMessage() => clearField(2); + + @$pb.TagNumber(3) + $core.List<$0.Any> get details => $_getList(2); +}
diff --git a/grpc/lib/src/generated/google/rpc/status.pbenum.dart b/grpc/lib/src/generated/google/rpc/status.pbenum.dart new file mode 100644 index 0000000..290e941 --- /dev/null +++ b/grpc/lib/src/generated/google/rpc/status.pbenum.dart
@@ -0,0 +1,6 @@ +/// +// Generated code. Do not modify. +// source: google/rpc/status.proto +// +// @dart = 2.12 +// ignore_for_file: annotate_overrides,camel_case_types,unnecessary_const,non_constant_identifier_names,library_prefixes,unused_import,unused_shown_name,return_of_invalid_type,unnecessary_this,prefer_final_fields
diff --git a/grpc/lib/src/generated/google/rpc/status.pbjson.dart b/grpc/lib/src/generated/google/rpc/status.pbjson.dart new file mode 100644 index 0000000..a89467d --- /dev/null +++ b/grpc/lib/src/generated/google/rpc/status.pbjson.dart
@@ -0,0 +1,24 @@ +/// +// Generated code. Do not modify. +// source: google/rpc/status.proto +// +// @dart = 2.12 +// ignore_for_file: annotate_overrides,camel_case_types,unnecessary_const,non_constant_identifier_names,library_prefixes,unused_import,unused_shown_name,return_of_invalid_type,unnecessary_this,prefer_final_fields + +import 'dart:core' as $core; + +const Status$json = const { + '1': 'Status', + '2': const [ + const {'1': 'code', '3': 1, '4': 1, '5': 5, '10': 'code'}, + const {'1': 'message', '3': 2, '4': 1, '5': 9, '10': 'message'}, + const { + '1': 'details', + '3': 3, + '4': 3, + '5': 11, + '6': '.google.protobuf.Any', + '10': 'details' + }, + ], +};
diff --git a/grpc/lib/src/protos/google/protobuf/any.proto b/grpc/lib/src/protos/google/protobuf/any.proto new file mode 100644 index 0000000..1c6a465 --- /dev/null +++ b/grpc/lib/src/protos/google/protobuf/any.proto
@@ -0,0 +1,158 @@ +// Protocol Buffers - Google's data interchange format +// Copyright 2008 Google Inc. All rights reserved. +// https://developers.google.com/protocol-buffers/ +// +// 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 Inc. 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. + +syntax = "proto3"; + +package google.protobuf; + +option csharp_namespace = "Google.Protobuf.WellKnownTypes"; +option go_package = "google.golang.org/protobuf/types/known/anypb"; +option java_package = "com.google.protobuf"; +option java_outer_classname = "AnyProto"; +option java_multiple_files = true; +option objc_class_prefix = "GPB"; + +// `Any` contains an arbitrary serialized protocol buffer message along with a +// URL that describes the type of the serialized message. +// +// Protobuf library provides support to pack/unpack Any values in the form +// of utility functions or additional generated methods of the Any type. +// +// Example 1: Pack and unpack a message in C++. +// +// Foo foo = ...; +// Any any; +// any.PackFrom(foo); +// ... +// if (any.UnpackTo(&foo)) { +// ... +// } +// +// Example 2: Pack and unpack a message in Java. +// +// Foo foo = ...; +// Any any = Any.pack(foo); +// ... +// if (any.is(Foo.class)) { +// foo = any.unpack(Foo.class); +// } +// +// Example 3: Pack and unpack a message in Python. +// +// foo = Foo(...) +// any = Any() +// any.Pack(foo) +// ... +// if any.Is(Foo.DESCRIPTOR): +// any.Unpack(foo) +// ... +// +// Example 4: Pack and unpack a message in Go +// +// foo := &pb.Foo{...} +// any, err := anypb.New(foo) +// if err != nil { +// ... +// } +// ... +// foo := &pb.Foo{} +// if err := any.UnmarshalTo(foo); err != nil { +// ... +// } +// +// The pack methods provided by protobuf library will by default use +// 'type.googleapis.com/full.type.name' as the type URL and the unpack +// methods only use the fully qualified type name after the last '/' +// in the type URL, for example "foo.bar.com/x/y.z" will yield type +// name "y.z". +// +// +// JSON +// ==== +// The JSON representation of an `Any` value uses the regular +// representation of the deserialized, embedded message, with an +// additional field `@type` which contains the type URL. Example: +// +// package google.profile; +// message Person { +// string first_name = 1; +// string last_name = 2; +// } +// +// { +// "@type": "type.googleapis.com/google.profile.Person", +// "firstName": <string>, +// "lastName": <string> +// } +// +// If the embedded message type is well-known and has a custom JSON +// representation, that representation will be embedded adding a field +// `value` which holds the custom JSON in addition to the `@type` +// field. Example (for message [google.protobuf.Duration][]): +// +// { +// "@type": "type.googleapis.com/google.protobuf.Duration", +// "value": "1.212s" +// } +// +message Any { + // A URL/resource name that uniquely identifies the type of the serialized + // protocol buffer message. This string must contain at least + // one "/" character. The last segment of the URL's path must represent + // the fully qualified name of the type (as in + // `path/google.protobuf.Duration`). The name should be in a canonical form + // (e.g., leading "." is not accepted). + // + // In practice, teams usually precompile into the binary all types that they + // expect it to use in the context of Any. However, for URLs which use the + // scheme `http`, `https`, or no scheme, one can optionally set up a type + // server that maps type URLs to message definitions as follows: + // + // * If no scheme is provided, `https` is assumed. + // * An HTTP GET on the URL must yield a [google.protobuf.Type][] + // value in binary format, or produce an error. + // * Applications are allowed to cache lookup results based on the + // URL, or have them precompiled into a binary to avoid any + // lookup. Therefore, binary compatibility needs to be preserved + // on changes to types. (Use versioned type names to manage + // breaking changes.) + // + // Note: this functionality is not currently available in the official + // protobuf release, and it is not used for type URLs beginning with + // type.googleapis.com. + // + // Schemes other than `http`, `https` (or the empty scheme) might be + // used with implementation specific semantics. + // + string type_url = 1; + + // Must be a valid serialized protocol buffer of the above specified type. + bytes value = 2; +} \ No newline at end of file
diff --git a/grpc/lib/src/protos/google/protobuf/duration.proto b/grpc/lib/src/protos/google/protobuf/duration.proto new file mode 100644 index 0000000..cb7cf0e --- /dev/null +++ b/grpc/lib/src/protos/google/protobuf/duration.proto
@@ -0,0 +1,116 @@ +// Protocol Buffers - Google's data interchange format +// Copyright 2008 Google Inc. All rights reserved. +// https://developers.google.com/protocol-buffers/ +// +// 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 Inc. 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. + +syntax = "proto3"; + +package google.protobuf; + +option csharp_namespace = "Google.Protobuf.WellKnownTypes"; +option cc_enable_arenas = true; +option go_package = "google.golang.org/protobuf/types/known/durationpb"; +option java_package = "com.google.protobuf"; +option java_outer_classname = "DurationProto"; +option java_multiple_files = true; +option objc_class_prefix = "GPB"; + +// A Duration represents a signed, fixed-length span of time represented +// as a count of seconds and fractions of seconds at nanosecond +// resolution. It is independent of any calendar and concepts like "day" +// or "month". It is related to Timestamp in that the difference between +// two Timestamp values is a Duration and it can be added or subtracted +// from a Timestamp. Range is approximately +-10,000 years. +// +// # Examples +// +// Example 1: Compute Duration from two Timestamps in pseudo code. +// +// Timestamp start = ...; +// Timestamp end = ...; +// Duration duration = ...; +// +// duration.seconds = end.seconds - start.seconds; +// duration.nanos = end.nanos - start.nanos; +// +// if (duration.seconds < 0 && duration.nanos > 0) { +// duration.seconds += 1; +// duration.nanos -= 1000000000; +// } else if (duration.seconds > 0 && duration.nanos < 0) { +// duration.seconds -= 1; +// duration.nanos += 1000000000; +// } +// +// Example 2: Compute Timestamp from Timestamp + Duration in pseudo code. +// +// Timestamp start = ...; +// Duration duration = ...; +// Timestamp end = ...; +// +// end.seconds = start.seconds + duration.seconds; +// end.nanos = start.nanos + duration.nanos; +// +// if (end.nanos < 0) { +// end.seconds -= 1; +// end.nanos += 1000000000; +// } else if (end.nanos >= 1000000000) { +// end.seconds += 1; +// end.nanos -= 1000000000; +// } +// +// Example 3: Compute Duration from datetime.timedelta in Python. +// +// td = datetime.timedelta(days=3, minutes=10) +// duration = Duration() +// duration.FromTimedelta(td) +// +// # JSON Mapping +// +// In JSON format, the Duration type is encoded as a string rather than an +// object, where the string ends in the suffix "s" (indicating seconds) and +// is preceded by the number of seconds, with nanoseconds expressed as +// fractional seconds. For example, 3 seconds with 0 nanoseconds should be +// encoded in JSON format as "3s", while 3 seconds and 1 nanosecond should +// be expressed in JSON format as "3.000000001s", and 3 seconds and 1 +// microsecond should be expressed in JSON format as "3.000001s". +// +// +message Duration { + // Signed seconds of the span of time. Must be from -315,576,000,000 + // to +315,576,000,000 inclusive. Note: these bounds are computed from: + // 60 sec/min * 60 min/hr * 24 hr/day * 365.25 days/year * 10000 years + int64 seconds = 1; + + // Signed fractions of a second at nanosecond resolution of the span + // of time. Durations less than one second are represented with a 0 + // `seconds` field and a positive or negative `nanos` field. For durations + // of one second or more, a non-zero value for the `nanos` field must be + // of the same sign as the `seconds` field. Must be from -999,999,999 + // to +999,999,999 inclusive. + int32 nanos = 2; +} \ No newline at end of file
diff --git a/grpc/lib/src/protos/google/rpc/code.proto b/grpc/lib/src/protos/google/rpc/code.proto new file mode 100644 index 0000000..d115da1 --- /dev/null +++ b/grpc/lib/src/protos/google/rpc/code.proto
@@ -0,0 +1,186 @@ +// Copyright 2020 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +syntax = "proto3"; + +package google.rpc; + +option go_package = "google.golang.org/genproto/googleapis/rpc/code;code"; +option java_multiple_files = true; +option java_outer_classname = "CodeProto"; +option java_package = "com.google.rpc"; +option objc_class_prefix = "RPC"; + +// The canonical error codes for gRPC APIs. +// +// +// Sometimes multiple error codes may apply. Services should return +// the most specific error code that applies. For example, prefer +// `OUT_OF_RANGE` over `FAILED_PRECONDITION` if both codes apply. +// Similarly prefer `NOT_FOUND` or `ALREADY_EXISTS` over `FAILED_PRECONDITION`. +enum Code { + // Not an error; returned on success + // + // HTTP Mapping: 200 OK + OK = 0; + + // The operation was cancelled, typically by the caller. + // + // HTTP Mapping: 499 Client Closed Request + CANCELLED = 1; + + // Unknown error. For example, this error may be returned when + // a `Status` value received from another address space belongs to + // an error space that is not known in this address space. Also + // errors raised by APIs that do not return enough error information + // may be converted to this error. + // + // HTTP Mapping: 500 Internal Server Error + UNKNOWN = 2; + + // The client specified an invalid argument. Note that this differs + // from `FAILED_PRECONDITION`. `INVALID_ARGUMENT` indicates arguments + // that are problematic regardless of the state of the system + // (e.g., a malformed file name). + // + // HTTP Mapping: 400 Bad Request + INVALID_ARGUMENT = 3; + + // The deadline expired before the operation could complete. For operations + // that change the state of the system, this error may be returned + // even if the operation has completed successfully. For example, a + // successful response from a server could have been delayed long + // enough for the deadline to expire. + // + // HTTP Mapping: 504 Gateway Timeout + DEADLINE_EXCEEDED = 4; + + // Some requested entity (e.g., file or directory) was not found. + // + // Note to server developers: if a request is denied for an entire class + // of users, such as gradual feature rollout or undocumented whitelist, + // `NOT_FOUND` may be used. If a request is denied for some users within + // a class of users, such as user-based access control, `PERMISSION_DENIED` + // must be used. + // + // HTTP Mapping: 404 Not Found + NOT_FOUND = 5; + + // The entity that a client attempted to create (e.g., file or directory) + // already exists. + // + // HTTP Mapping: 409 Conflict + ALREADY_EXISTS = 6; + + // The caller does not have permission to execute the specified + // operation. `PERMISSION_DENIED` must not be used for rejections + // caused by exhausting some resource (use `RESOURCE_EXHAUSTED` + // instead for those errors). `PERMISSION_DENIED` must not be + // used if the caller can not be identified (use `UNAUTHENTICATED` + // instead for those errors). This error code does not imply the + // request is valid or the requested entity exists or satisfies + // other pre-conditions. + // + // HTTP Mapping: 403 Forbidden + PERMISSION_DENIED = 7; + + // The request does not have valid authentication credentials for the + // operation. + // + // HTTP Mapping: 401 Unauthorized + UNAUTHENTICATED = 16; + + // Some resource has been exhausted, perhaps a per-user quota, or + // perhaps the entire file system is out of space. + // + // HTTP Mapping: 429 Too Many Requests + RESOURCE_EXHAUSTED = 8; + + // The operation was rejected because the system is not in a state + // required for the operation's execution. For example, the directory + // to be deleted is non-empty, an rmdir operation is applied to + // a non-directory, etc. + // + // Service implementors can use the following guidelines to decide + // between `FAILED_PRECONDITION`, `ABORTED`, and `UNAVAILABLE`: + // (a) Use `UNAVAILABLE` if the client can retry just the failing call. + // (b) Use `ABORTED` if the client should retry at a higher level + // (e.g., when a client-specified test-and-set fails, indicating the + // client should restart a read-modify-write sequence). + // (c) Use `FAILED_PRECONDITION` if the client should not retry until + // the system state has been explicitly fixed. E.g., if an "rmdir" + // fails because the directory is non-empty, `FAILED_PRECONDITION` + // should be returned since the client should not retry unless + // the files are deleted from the directory. + // + // HTTP Mapping: 400 Bad Request + FAILED_PRECONDITION = 9; + + // The operation was aborted, typically due to a concurrency issue such as + // a sequencer check failure or transaction abort. + // + // See the guidelines above for deciding between `FAILED_PRECONDITION`, + // `ABORTED`, and `UNAVAILABLE`. + // + // HTTP Mapping: 409 Conflict + ABORTED = 10; + + // The operation was attempted past the valid range. E.g., seeking or + // reading past end-of-file. + // + // Unlike `INVALID_ARGUMENT`, this error indicates a problem that may + // be fixed if the system state changes. For example, a 32-bit file + // system will generate `INVALID_ARGUMENT` if asked to read at an + // offset that is not in the range [0,2^32-1], but it will generate + // `OUT_OF_RANGE` if asked to read from an offset past the current + // file size. + // + // There is a fair bit of overlap between `FAILED_PRECONDITION` and + // `OUT_OF_RANGE`. We recommend using `OUT_OF_RANGE` (the more specific + // error) when it applies so that callers who are iterating through + // a space can easily look for an `OUT_OF_RANGE` error to detect when + // they are done. + // + // HTTP Mapping: 400 Bad Request + OUT_OF_RANGE = 11; + + // The operation is not implemented or is not supported/enabled in this + // service. + // + // HTTP Mapping: 501 Not Implemented + UNIMPLEMENTED = 12; + + // Internal errors. This means that some invariants expected by the + // underlying system have been broken. This error code is reserved + // for serious errors. + // + // HTTP Mapping: 500 Internal Server Error + INTERNAL = 13; + + // The service is currently unavailable. This is most likely a + // transient condition, which can be corrected by retrying with + // a backoff. Note that it is not always safe to retry + // non-idempotent operations. + // + // See the guidelines above for deciding between `FAILED_PRECONDITION`, + // `ABORTED`, and `UNAVAILABLE`. + // + // HTTP Mapping: 503 Service Unavailable + UNAVAILABLE = 14; + + // Unrecoverable data loss or corruption. + // + // HTTP Mapping: 500 Internal Server Error + DATA_LOSS = 15; +} \ No newline at end of file
diff --git a/grpc/lib/src/protos/google/rpc/error_details.proto b/grpc/lib/src/protos/google/rpc/error_details.proto new file mode 100644 index 0000000..981d18c --- /dev/null +++ b/grpc/lib/src/protos/google/rpc/error_details.proto
@@ -0,0 +1,249 @@ +// Copyright 2020 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +syntax = "proto3"; + +package google.rpc; + +import "google/protobuf/duration.proto"; + +option go_package = "google.golang.org/genproto/googleapis/rpc/errdetails;errdetails"; +option java_multiple_files = true; +option java_outer_classname = "ErrorDetailsProto"; +option java_package = "com.google.rpc"; +option objc_class_prefix = "RPC"; + +// Describes when the clients can retry a failed request. Clients could ignore +// the recommendation here or retry when this information is missing from error +// responses. +// +// It's always recommended that clients should use exponential backoff when +// retrying. +// +// Clients should wait until `retry_delay` amount of time has passed since +// receiving the error response before retrying. If retrying requests also +// fail, clients should use an exponential backoff scheme to gradually increase +// the delay between retries based on `retry_delay`, until either a maximum +// number of retries have been reached or a maximum retry delay cap has been +// reached. +message RetryInfo { + // Clients should wait at least this long between retrying the same request. + google.protobuf.Duration retry_delay = 1; +} + +// Describes additional debugging info. +message DebugInfo { + // The stack trace entries indicating where the error occurred. + repeated string stack_entries = 1; + + // Additional debugging information provided by the server. + string detail = 2; +} + +// Describes how a quota check failed. +// +// For example if a daily limit was exceeded for the calling project, +// a service could respond with a QuotaFailure detail containing the project +// id and the description of the quota limit that was exceeded. If the +// calling project hasn't enabled the service in the developer console, then +// a service could respond with the project id and set `service_disabled` +// to true. +// +// Also see RetryInfo and Help types for other details about handling a +// quota failure. +message QuotaFailure { + // A message type used to describe a single quota violation. For example, a + // daily quota or a custom quota that was exceeded. + message Violation { + // The subject on which the quota check failed. + // For example, "clientip:<ip address of client>" or "project:<Google + // developer project id>". + string subject = 1; + + // A description of how the quota check failed. Clients can use this + // description to find more about the quota configuration in the service's + // public documentation, or find the relevant quota limit to adjust through + // developer console. + // + // For example: "Service disabled" or "Daily Limit for read operations + // exceeded". + string description = 2; + } + + // Describes all quota violations. + repeated Violation violations = 1; +} + +// Describes the cause of the error with structured details. +// +// Example of an error when contacting the "pubsub.googleapis.com" API when it +// is not enabled: +// +// { "reason": "API_DISABLED" +// "domain": "googleapis.com" +// "metadata": { +// "resource": "projects/123", +// "service": "pubsub.googleapis.com" +// } +// } +// +// This response indicates that the pubsub.googleapis.com API is not enabled. +// +// Example of an error that is returned when attempting to create a Spanner +// instance in a region that is out of stock: +// +// { "reason": "STOCKOUT" +// "domain": "spanner.googleapis.com", +// "metadata": { +// "availableRegions": "us-central1,us-east2" +// } +// } +message ErrorInfo { + // The reason of the error. This is a constant value that identifies the + // proximate cause of the error. Error reasons are unique within a particular + // domain of errors. This should be at most 63 characters and match + // /[A-Z0-9_]+/. + string reason = 1; + + // The logical grouping to which the "reason" belongs. The error domain + // is typically the registered service name of the tool or product that + // generates the error. Example: "pubsub.googleapis.com". If the error is + // generated by some common infrastructure, the error domain must be a + // globally unique value that identifies the infrastructure. For Google API + // infrastructure, the error domain is "googleapis.com". + string domain = 2; + + // Additional structured details about this error. + // + // Keys should match /[a-zA-Z0-9-_]/ and be limited to 64 characters in + // length. When identifying the current value of an exceeded limit, the units + // should be contained in the key, not the value. For example, rather than + // {"instanceLimit": "100/request"}, should be returned as, + // {"instanceLimitPerRequest": "100"}, if the client exceeds the number of + // instances that can be created in a single (batch) request. + map<string, string> metadata = 3; +} + +// Describes what preconditions have failed. +// +// For example, if an RPC failed because it required the Terms of Service to be +// acknowledged, it could list the terms of service violation in the +// PreconditionFailure message. +message PreconditionFailure { + // A message type used to describe a single precondition failure. + message Violation { + // The type of PreconditionFailure. We recommend using a service-specific + // enum type to define the supported precondition violation subjects. For + // example, "TOS" for "Terms of Service violation". + string type = 1; + + // The subject, relative to the type, that failed. + // For example, "google.com/cloud" relative to the "TOS" type would indicate + // which terms of service is being referenced. + string subject = 2; + + // A description of how the precondition failed. Developers can use this + // description to understand how to fix the failure. + // + // For example: "Terms of service not accepted". + string description = 3; + } + + // Describes all precondition violations. + repeated Violation violations = 1; +} + +// Describes violations in a client request. This error type focuses on the +// syntactic aspects of the request. +message BadRequest { + // A message type used to describe a single bad request field. + message FieldViolation { + // A path leading to a field in the request body. The value will be a + // sequence of dot-separated identifiers that identify a protocol buffer + // field. E.g., "field_violations.field" would identify this field. + string field = 1; + + // A description of why the request element is bad. + string description = 2; + } + + // Describes all violations in a client request. + repeated FieldViolation field_violations = 1; +} + +// Contains metadata about the request that clients can attach when filing a bug +// or providing other forms of feedback. +message RequestInfo { + // An opaque string that should only be interpreted by the service generating + // it. For example, it can be used to identify requests in the service's logs. + string request_id = 1; + + // Any data that was used to serve this request. For example, an encrypted + // stack trace that can be sent back to the service provider for debugging. + string serving_data = 2; +} + +// Describes the resource that is being accessed. +message ResourceInfo { + // A name for the type of resource being accessed, e.g. "sql table", + // "cloud storage bucket", "file", "Google calendar"; or the type URL + // of the resource: e.g. "type.googleapis.com/google.pubsub.v1.Topic". + string resource_type = 1; + + // The name of the resource being accessed. For example, a shared calendar + // name: "example.com_4fghdhgsrgh@group.calendar.google.com", if the current + // error is [google.rpc.Code.PERMISSION_DENIED][google.rpc.Code.PERMISSION_DENIED]. + string resource_name = 2; + + // The owner of the resource (optional). + // For example, "user:<owner email>" or "project:<Google developer project + // id>". + string owner = 3; + + // Describes what error is encountered when accessing this resource. + // For example, updating a cloud project may require the `writer` permission + // on the developer console project. + string description = 4; +} + +// Provides links to documentation or for performing an out of band action. +// +// For example, if a quota check failed with an error indicating the calling +// project hasn't enabled the accessed service, this can contain a URL pointing +// directly to the right place in the developer console to flip the bit. +message Help { + // Describes a URL link. + message Link { + // Describes what the link offers. + string description = 1; + + // The URL of the link. + string url = 2; + } + + // URL(s) pointing to additional information on handling the current error. + repeated Link links = 1; +} + +// Provides a localized error message that is safe to return to the user +// which can be attached to an RPC error. +message LocalizedMessage { + // The locale used following the specification defined at + // http://www.rfc-editor.org/rfc/bcp/bcp47.txt. + // Examples are: "en-US", "fr-CH", "es-MX" + string locale = 1; + + // The localized error message in the above locale. + string message = 2; +} \ No newline at end of file
diff --git a/grpc/lib/src/protos/google/rpc/status.proto b/grpc/lib/src/protos/google/rpc/status.proto new file mode 100644 index 0000000..5bd51aa --- /dev/null +++ b/grpc/lib/src/protos/google/rpc/status.proto
@@ -0,0 +1,47 @@ +// Copyright 2020 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +syntax = "proto3"; + +package google.rpc; + +import "google/protobuf/any.proto"; + +option cc_enable_arenas = true; +option go_package = "google.golang.org/genproto/googleapis/rpc/status;status"; +option java_multiple_files = true; +option java_outer_classname = "StatusProto"; +option java_package = "com.google.rpc"; +option objc_class_prefix = "RPC"; + +// The `Status` type defines a logical error model that is suitable for +// different programming environments, including REST APIs and RPC APIs. It is +// used by [gRPC](https://github.com/grpc). Each `Status` message contains +// three pieces of data: error code, error message, and error details. +// +// You can find out more about this error model and how to work with it in the +// [API Design Guide](https://cloud.google.com/apis/design/errors). +message Status { + // The status code, which should be an enum value of [google.rpc.Code][google.rpc.Code]. + int32 code = 1; + + // A developer-facing error message, which should be in English. Any + // user-facing error message should be localized and sent in the + // [google.rpc.Status.details][google.rpc.Status.details] field, or localized by the client. + string message = 2; + + // A list of messages that carry the error details. There is a common set of + // message types for APIs to use. + repeated google.protobuf.Any details = 3; +} \ No newline at end of file
diff --git a/grpc/lib/src/server/call.dart b/grpc/lib/src/server/call.dart new file mode 100644 index 0000000..a5624ae --- /dev/null +++ b/grpc/lib/src/server/call.dart
@@ -0,0 +1,59 @@ +// Copyright (c) 2017, the gRPC project authors. Please see the AUTHORS file +// for details. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import '../shared/io_bits/io_bits.dart' show X509Certificate; + +/// Server-side context for a gRPC call. +/// +/// Gives the method handler access to custom metadata from the client, and +/// ability to set custom metadata on the header/trailer sent to the client. +abstract class ServiceCall { + /// Custom metadata from the client. + Map<String, String>? get clientMetadata; + + /// Custom metadata to be sent to the client. Will be [null] once the headers + /// have been sent, either when [sendHeaders] is called, or when the first + /// response message is sent. + Map<String, String>? get headers; + + /// Custom metadata to be sent to the client after all response messages. + Map<String, String>? get trailers; + + /// Deadline for this call. If the call is still active after this time, then + /// the client or server may cancel it. + DateTime? get deadline; + + /// Returns [true] if the [deadline] has been exceeded. + bool get isTimedOut; + + /// Returns [true] if the client has canceled this call. + bool get isCanceled; + + /// Returns the client certificate if it is requested and available + X509Certificate? get clientCertificate; + + /// Send response headers. This is done automatically before sending the first + /// response message, but can be done manually before the first response is + /// ready, if necessary. + void sendHeaders(); + + /// Send response trailers. A trailer indicating success ([status] == 0) will + /// be sent automatically when all responses are sent. This method can be used + /// to send a different status code, if needed. + /// + /// The call will be closed after calling this method, and no further + /// responses can be sent. + void sendTrailers({int? status, String? message}); +}
diff --git a/grpc/lib/src/server/handler.dart b/grpc/lib/src/server/handler.dart new file mode 100644 index 0000000..798c8af --- /dev/null +++ b/grpc/lib/src/server/handler.dart
@@ -0,0 +1,427 @@ +// Copyright (c) 2017, the gRPC project authors. Please see the AUTHORS file +// for details. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import 'dart:async'; +import 'dart:convert'; + +import 'package:http2/transport.dart'; + +import '../shared/codec.dart'; +import '../shared/codec_registry.dart'; +import '../shared/io_bits/io_bits.dart' show X509Certificate; +import '../shared/message.dart'; +import '../shared/status.dart'; +import '../shared/streams.dart'; +import '../shared/timeout.dart'; +import 'call.dart'; +import 'interceptor.dart'; +import 'service.dart'; + +/// Handles an incoming gRPC call. +class ServerHandler_ extends ServiceCall { + final ServerTransportStream _stream; + final Service? Function(String service) _serviceLookup; + final List<Interceptor> _interceptors; + final CodecRegistry? _codecRegistry; + + // ignore: cancel_subscriptions + StreamSubscription<GrpcMessage>? _incomingSubscription; + + late Service _service; + late ServiceMethod _descriptor; + + Map<String, String>? _clientMetadata; + Codec? _callEncodingCodec; + + StreamController? _requests; + bool _hasReceivedRequest = false; + + late Stream _responses; + StreamSubscription? _responseSubscription; + bool _headersSent = false; + + Map<String, String>? _customHeaders = {}; + Map<String, String>? _customTrailers = {}; + + DateTime? _deadline; + bool _isCanceled = false; + bool _isTimedOut = false; + Timer? _timeoutTimer; + final X509Certificate? _clientCertificate; + + ServerHandler_(this._serviceLookup, this._stream, this._interceptors, + this._codecRegistry, + [this._clientCertificate]); + + @override + DateTime? get deadline => _deadline; + + @override + bool get isCanceled => _isCanceled; + + @override + bool get isTimedOut => _isTimedOut; + + @override + Map<String, String>? get clientMetadata => _clientMetadata; + + @override + Map<String, String>? get headers => _customHeaders; + + @override + Map<String, String>? get trailers => _customTrailers; + + @override + X509Certificate? get clientCertificate => _clientCertificate; + + void handle() { + _stream.onTerminated = (_) => cancel(); + + _incomingSubscription = _stream.incomingMessages + .transform(GrpcHttpDecoder()) + .transform(grpcDecompressor(codecRegistry: _codecRegistry)) + .listen(_onDataIdle, + onError: _onError, onDone: _onDoneError, cancelOnError: true); + _stream.outgoingMessages.done.then((_) { + cancel(); + }); + } + + /// Cancel response subscription, if active. If the stream exits with an + /// error, just ignore it. The client is long gone, so it doesn't care. + /// We need the catchError() handler here, since otherwise the error would + /// be an unhandled exception. + void _cancelResponseSubscription() { + _responseSubscription?.cancel().catchError((_) {}); + } + + // -- Idle state, incoming data -- + + void _onDataIdle(GrpcMessage headerMessage) async { + if (headerMessage is! GrpcMetadata) { + _sendError(GrpcError.unimplemented('Expected header frame')); + _sinkIncoming(); + return; + } + _incomingSubscription!.pause(); + + _clientMetadata = headerMessage.metadata; + final path = _clientMetadata![':path']!; + final pathSegments = path.split('/'); + if (pathSegments.length < 3) { + _sendError(GrpcError.unimplemented('Invalid path')); + _sinkIncoming(); + return; + } + final serviceName = pathSegments[1]; + final methodName = pathSegments[2]; + if (_codecRegistry != null) { + final acceptedEncodings = + clientMetadata!['grpc-accept-encoding']?.split(',') ?? []; + _callEncodingCodec = acceptedEncodings + .map(_codecRegistry!.lookup) + .firstWhere((c) => c != null, orElse: () => null); + } + + final service = _serviceLookup(serviceName); + final descriptor = service?.$lookupMethod(methodName); + if (descriptor == null) { + _sendError(GrpcError.unimplemented('Path $path not found')); + _sinkIncoming(); + return; + } + _service = service!; + _descriptor = descriptor; + + final error = await _applyInterceptors(); + if (error != null) { + _sendError(error); + _sinkIncoming(); + return; + } + + _startStreamingRequest(); + } + + GrpcError? _onMetadata() { + try { + _service.$onMetadata(this); + } on GrpcError catch (error) { + return error; + } catch (error) { + final grpcError = GrpcError.internal(error.toString()); + return grpcError; + } + return null; + } + + Future<GrpcError?> _applyInterceptors() async { + try { + for (final interceptor in _interceptors) { + final error = await interceptor(this, _descriptor); + if (error != null) { + return error; + } + } + } catch (error) { + final grpcError = GrpcError.internal(error.toString()); + return grpcError; + } + return null; + } + + void _startStreamingRequest() { + final requests = _descriptor.createRequestStream(_incomingSubscription!); + _requests = requests; + _incomingSubscription!.onData(_onDataActive); + + final error = _onMetadata(); + if (error != null) { + if (!requests.isClosed) { + requests + ..addError(error) + ..close(); + } + _sendError(error); + _onDone(); + _stream.terminate(); + return; + } + + _responses = _descriptor.handle(this, requests.stream); + + _responseSubscription = _responses.listen(_onResponse, + onError: _onResponseError, + onDone: _onResponseDone, + cancelOnError: true); + _incomingSubscription!.onData(_onDataActive); + _incomingSubscription!.onDone(_onDoneExpected); + + final timeout = fromTimeoutString(_clientMetadata!['grpc-timeout']); + if (timeout != null) { + _deadline = DateTime.now().add(timeout); + _timeoutTimer = Timer(timeout, _onTimedOut); + } + } + + void _onTimedOut() { + if (_isCanceled) return; + _isTimedOut = true; + _isCanceled = true; + final error = GrpcError.deadlineExceeded('Deadline exceeded'); + _sendError(error); + if (!_requests!.isClosed) { + _requests! + ..addError(error) + ..close(); + } + } + + // -- Active state, incoming data -- + + void _onDataActive(GrpcMessage message) { + if (message is! GrpcData) { + final error = GrpcError.unimplemented('Expected request'); + _sendError(error); + _requests! + ..addError(error) + ..close(); + return; + } + + if (_hasReceivedRequest && !_descriptor.streamingRequest) { + final error = GrpcError.unimplemented('Too many requests'); + _sendError(error); + _requests! + ..addError(error) + ..close(); + return; + } + + final data = message; + var request; + try { + request = _descriptor.deserialize(data.data); + } catch (error) { + final grpcError = + GrpcError.internal('Error deserializing request: $error'); + _sendError(grpcError); + _requests! + ..addError(grpcError) + ..close(); + return; + } + _requests!.add(request); + _hasReceivedRequest = true; + } + + // -- Active state, outgoing response data -- + + void _onResponse(response) { + try { + final bytes = _descriptor.serialize(response); + if (!_headersSent) { + sendHeaders(); + } + _stream.sendData(frame(bytes, _callEncodingCodec)); + } catch (error) { + final grpcError = GrpcError.internal('Error sending response: $error'); + if (!_requests!.isClosed) { + // If we can, alert the handler that things are going wrong. + _requests! + ..addError(grpcError) + ..close(); + } + _sendError(grpcError); + _cancelResponseSubscription(); + } + } + + void _onResponseDone() { + sendTrailers(); + } + + void _onResponseError(error) { + if (error is GrpcError) { + _sendError(error); + } else { + _sendError(GrpcError.unknown(error.toString())); + } + } + + @override + void sendHeaders() { + if (_headersSent) throw GrpcError.internal('Headers already sent'); + + _customHeaders! + ..remove(':status') + ..remove('content-type'); + + // TODO(jakobr): Should come from package:http2? + final outgoingHeadersMap = <String, String>{ + ':status': '200', + 'content-type': 'application/grpc', + if (_callEncodingCodec != null) + 'grpc-encoding': _callEncodingCodec!.encodingName, + }; + + outgoingHeadersMap.addAll(_customHeaders!); + _customHeaders = null; + + final outgoingHeaders = <Header>[]; + outgoingHeadersMap.forEach((key, value) => + outgoingHeaders.add(Header(ascii.encode(key), utf8.encode(value)))); + _stream.sendHeaders(outgoingHeaders); + _headersSent = true; + } + + @override + void sendTrailers({int? status = 0, String? message}) { + _timeoutTimer?.cancel(); + + final outgoingTrailersMap = <String, String>{}; + if (!_headersSent) { + // TODO(jakobr): Should come from package:http2? + outgoingTrailersMap[':status'] = '200'; + outgoingTrailersMap['content-type'] = 'application/grpc'; + + _customHeaders! + ..remove(':status') + ..remove('content-type'); + outgoingTrailersMap.addAll(_customHeaders!); + _customHeaders = null; + _headersSent = true; + } + _customTrailers! + ..remove(':status') + ..remove('content-type'); + outgoingTrailersMap.addAll(_customTrailers!); + _customTrailers = null; + outgoingTrailersMap['grpc-status'] = status.toString(); + if (message != null) { + outgoingTrailersMap['grpc-message'] = + Uri.encodeFull(message).replaceAll('%20', ' '); + } + + final outgoingTrailers = <Header>[]; + outgoingTrailersMap.forEach((key, value) => + outgoingTrailers.add(Header(ascii.encode(key), utf8.encode(value)))); + _stream.sendHeaders(outgoingTrailers, endStream: true); + // We're done! + _cancelResponseSubscription(); + _sinkIncoming(); + } + + // -- All states, incoming error / stream closed -- + + void _onError(error) { + // Exception from the incoming stream. Most likely a cancel request from the + // client, so we treat it as such. + _timeoutTimer?.cancel(); + _isCanceled = true; + if (_requests != null && !_requests!.isClosed) { + _requests!.addError(GrpcError.cancelled('Cancelled')); + } + _cancelResponseSubscription(); + _incomingSubscription!.cancel(); + _stream.terminate(); + } + + void _onDoneError() { + _sendError(GrpcError.unavailable('Request stream closed unexpectedly')); + _onDone(); + } + + void _onDoneExpected() { + if (!(_hasReceivedRequest || _descriptor.streamingRequest)) { + final error = GrpcError.unimplemented('No request received'); + _sendError(error); + _requests!.addError(error); + } + _onDone(); + } + + void _onDone() { + _requests?.close(); + _incomingSubscription!.cancel(); + } + + /// Sink incoming requests. This is used when an error has already been + /// reported, but we still need to consume the request stream from the client. + void _sinkIncoming() { + _incomingSubscription! + ..onData((_) {}) + ..onDone(_onDone); + } + + void _sendError(GrpcError error) { + sendTrailers(status: error.code, message: error.message); + } + + void cancel() { + _isCanceled = true; + _timeoutTimer?.cancel(); + _cancelResponseSubscription(); + } +} + +class ServerHandler extends ServerHandler_ { + ServerHandler(Service Function(String service) serviceLookup, stream, + [List<Interceptor> interceptors = const <Interceptor>[], + CodecRegistry? codecRegistry, + X509Certificate? clientCertificate]) + : super(serviceLookup, stream, interceptors, codecRegistry, + clientCertificate); +}
diff --git a/grpc/lib/src/server/interceptor.dart b/grpc/lib/src/server/interceptor.dart new file mode 100644 index 0000000..ee9286c --- /dev/null +++ b/grpc/lib/src/server/interceptor.dart
@@ -0,0 +1,14 @@ +import 'dart:async'; + +import '../shared/status.dart'; +import 'call.dart'; +import 'service.dart'; + +/// A gRPC Interceptor. +/// +/// An interceptor is called before the corresponding [ServiceMethod] invocation. +/// If the interceptor returns a [GrpcError], the error will be returned as a response and [ServiceMethod] wouldn't be called. +/// If the interceptor throws [Exception], [GrpcError.internal] with exception.toString() will be returned. +/// If the interceptor returns null, the corresponding [ServiceMethod] of [Service] will be called. +typedef Interceptor = FutureOr<GrpcError?> Function( + ServiceCall call, ServiceMethod method);
diff --git a/grpc/lib/src/server/server.dart b/grpc/lib/src/server/server.dart new file mode 100644 index 0000000..b22ac81 --- /dev/null +++ b/grpc/lib/src/server/server.dart
@@ -0,0 +1,256 @@ +// Copyright (c) 2017, the gRPC project authors. Please see the AUTHORS file +// for details. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import 'dart:async'; +import 'dart:io'; + +import 'package:http2/transport.dart'; +import 'package:meta/meta.dart'; + +import '../shared/codec_registry.dart'; +import '../shared/io_bits/io_bits.dart' as io_bits; +import '../shared/security.dart'; +import 'handler.dart'; +import 'interceptor.dart'; +import 'service.dart'; + +/// Wrapper around grpc_server_credentials, a way to authenticate a server. +abstract class ServerCredentials { + /// Validates incoming connection. Returns [true] if connection is + /// allowed to proceed. + bool validateClient(Socket socket) => true; + + /// Creates [SecurityContext] from these credentials if possible. + /// Otherwise returns [null]. + SecurityContext? get securityContext; +} + +/// Set of credentials that only allows local TCP connections. +class ServerLocalCredentials extends ServerCredentials { + @override + bool validateClient(Socket socket) => socket.remoteAddress.isLoopback; + + @override + SecurityContext? get securityContext => null; +} + +class ServerTlsCredentials extends ServerCredentials { + final List<int>? certificate; + final String? certificatePassword; + final List<int>? privateKey; + final String? privateKeyPassword; + + /// TLS credentials for a [Server]. + /// + /// If the [certificate] or [privateKey] is encrypted, the password must also + /// be provided. + ServerTlsCredentials( + {this.certificate, + this.certificatePassword, + this.privateKey, + this.privateKeyPassword}); + + @override + SecurityContext get securityContext { + final context = createSecurityContext(true); + if (privateKey != null) { + context.usePrivateKeyBytes(privateKey!, password: privateKeyPassword); + } + if (certificate != null) { + context.useCertificateChainBytes(certificate!, + password: certificatePassword); + } + return context; + } + + @override + bool validateClient(Socket socket) => true; +} + +/// A gRPC server that serves via provided [ServerTransportConnection]s. +/// +/// Unlike [Server], the caller has the responsibility of configuring and +/// managing the connection from a client. +class ConnectionServer { + final Map<String, Service> _services = {}; + final List<Interceptor> _interceptors; + final CodecRegistry? _codecRegistry; + + final _connections = <ServerTransportConnection>[]; + + /// Create a server for the given [services]. + ConnectionServer( + List<Service> services, [ + List<Interceptor> interceptors = const <Interceptor>[], + CodecRegistry? codecRegistry, + ]) : _codecRegistry = codecRegistry, + _interceptors = interceptors { + for (final service in services) { + _services[service.$name] = service; + } + } + + Service? lookupService(String service) => _services[service]; + + Future<void> serveConnection(ServerTransportConnection connection, + [X509Certificate? clientCertificate]) async { + _connections.add(connection); + ServerHandler_? handler; + // TODO(jakobr): Set active state handlers, close connection after idle + // timeout. + connection.incomingStreams.listen((stream) { + handler = serveStream_(stream, clientCertificate); + }, onError: (error, stackTrace) { + if (error is Error) { + Zone.current.handleUncaughtError(error, stackTrace); + } + }, onDone: () { + // TODO(sigurdm): This is not correct behavior in the presence of + // half-closed tcp streams. + // Half-closed streams seems to not be fully supported by package:http2. + // https://github.com/dart-lang/http2/issues/42 + handler?.cancel(); + _connections.remove(connection); + }); + } + + @visibleForTesting + ServerHandler_ serveStream_(ServerTransportStream stream, + [X509Certificate? clientCertificate]) { + return ServerHandler_( + lookupService, stream, _interceptors, _codecRegistry, + // ignore: unnecessary_cast + clientCertificate as io_bits.X509Certificate?, + )..handle(); + } +} + +/// A gRPC server. +/// +/// Listens for incoming RPCs, dispatching them to the right [Service] handler. +class Server extends ConnectionServer { + ServerSocket? _insecureServer; + SecureServerSocket? _secureServer; + + /// Create a server for the given [services]. + Server( + List<Service> services, [ + List<Interceptor> interceptors = const <Interceptor>[], + CodecRegistry? codecRegistry, + ]) : super(services, interceptors, codecRegistry); + + /// The port that the server is listening on, or `null` if the server is not + /// active. + int? get port { + if (_secureServer != null) return _secureServer!.port; + if (_insecureServer != null) return _insecureServer!.port; + return null; + } + + @override + Service? lookupService(String service) => _services[service]; + + /// Starts the [Server] with the given options. + /// [address] can be either a [String] or an [InternetAddress], in the latter + /// case it can be a Unix Domain Socket address. + /// + /// If [port] is [null] then it defaults to `80` for non-secure and `443` for + /// secure variants. Pass `0` for [port] to let OS select a port for the + /// server. + Future<void> serve({ + dynamic address, + int? port, + ServerCredentials? security, + ServerSettings? http2ServerSettings, + int backlog = 0, + bool v6Only = false, + bool shared = false, + bool requestClientCertificate = false, + bool requireClientCertificate = false, + }) async { + // TODO(dart-lang/grpc-dart#9): Handle HTTP/1.1 upgrade to h2c, if allowed. + Stream<Socket>? server; + final securityContext = security?.securityContext; + if (securityContext != null) { + _secureServer = await SecureServerSocket.bind( + address ?? InternetAddress.anyIPv4, port ?? 443, securityContext, + backlog: backlog, + shared: shared, + v6Only: v6Only, + requestClientCertificate: requestClientCertificate, + requireClientCertificate: requireClientCertificate); + server = _secureServer; + } else { + _insecureServer = await ServerSocket.bind( + address ?? InternetAddress.anyIPv4, + port ?? 80, + backlog: backlog, + shared: shared, + v6Only: v6Only, + ); + server = _insecureServer; + } + server!.listen((socket) { + // Don't wait for io buffers to fill up before sending requests. + if (socket.address.type != InternetAddressType.unix) { + socket.setOption(SocketOption.tcpNoDelay, true); + } + X509Certificate? clientCertificate; + if (socket is SecureSocket) { + clientCertificate = socket.peerCertificate; + } + final connection = ServerTransportConnection.viaSocket(socket, + settings: http2ServerSettings); + serveConnection(connection, clientCertificate); + }, onError: (error, stackTrace) { + if (error is Error) { + Zone.current.handleUncaughtError(error, stackTrace); + } + }); + } + + @override + @visibleForTesting + ServerHandler_ serveStream_(ServerTransportStream stream, + [X509Certificate? clientCertificate]) { + return ServerHandler_( + lookupService, + stream, + _interceptors, + _codecRegistry, + // ignore: unnecessary_cast + clientCertificate as io_bits.X509Certificate?, + )..handle(); + } + + @Deprecated( + 'This is internal functionality, and will be removed in next major version.') + void serveStream(ServerTransportStream stream) { + serveStream_(stream); + } + + Future<void> shutdown() async { + final done = _connections.map((connection) => connection.finish()).toList(); + if (_insecureServer != null) { + done.add(_insecureServer!.close()); + } + if (_secureServer != null) { + done.add(_secureServer!.close()); + } + await Future.wait(done); + _insecureServer = null; + _secureServer = null; + } +}
diff --git a/grpc/lib/src/server/service.dart b/grpc/lib/src/server/service.dart new file mode 100644 index 0000000..ac5e37a --- /dev/null +++ b/grpc/lib/src/server/service.dart
@@ -0,0 +1,111 @@ +// Copyright (c) 2017, the gRPC project authors. Please see the AUTHORS file +// for details. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import 'dart:async'; + +import '../shared/status.dart'; +import 'call.dart'; + +/// Definition of a gRPC service method. +class ServiceMethod<Q, R> { + final String name; + + final bool streamingRequest; + final bool streamingResponse; + + final Q Function(List<int> request) requestDeserializer; + final List<int> Function(R response) responseSerializer; + + final Function handler; + + ServiceMethod( + this.name, + this.handler, + this.streamingRequest, + this.streamingResponse, + this.requestDeserializer, + this.responseSerializer); + + StreamController<Q> createRequestStream(StreamSubscription incoming) => + StreamController<Q>( + onListen: incoming.resume, + onPause: incoming.pause, + onResume: incoming.resume); + + Q deserialize(List<int> data) => requestDeserializer(data); + + List<int> serialize(dynamic response) => responseSerializer(response as R); + + Stream<R> handle(ServiceCall call, Stream<Q> requests) { + if (streamingResponse) { + if (streamingRequest) { + return handler(call, requests); + } else { + return handler(call, _toSingleFuture(requests)); + } + } else { + final response = streamingRequest + ? handler(call, requests) + : handler(call, _toSingleFuture(requests)); + return response.asStream(); + } + } + + Future<Q> _toSingleFuture(Stream<Q> stream) { + Q _ensureOnlyOneRequest(Q? previous, Q element) { + if (previous != null) { + throw GrpcError.unimplemented('More than one request received'); + } + return element; + } + + Q _ensureOneRequest(Q? value) { + if (value == null) throw GrpcError.unimplemented('No requests received'); + return value; + } + + final future = + stream.fold<Q?>(null, _ensureOnlyOneRequest).then(_ensureOneRequest); + // Make sure errors on the future aren't unhandled, but return the original + // future so the request handler can also get the error. + _awaitAndCatch(future); + return future; + } + + void _awaitAndCatch(Future<Q> f) async { + try { + await f; + } catch (_) {} + } +} + +/// Definition of a gRPC service. +abstract class Service { + final Map<String, ServiceMethod> _$methods = {}; + + String get $name; + + void $addMethod(ServiceMethod method) { + _$methods[method.name] = method; + } + + /// Client metadata handler. + /// + /// Services can override this method to provide common handling of incoming + /// metadata from the client. + void $onMetadata(ServiceCall context) {} + + ServiceMethod? $lookupMethod(String name) => _$methods[name]; +}
diff --git a/grpc/lib/src/shared/api.dart b/grpc/lib/src/shared/api.dart new file mode 100644 index 0000000..5a509f1 --- /dev/null +++ b/grpc/lib/src/shared/api.dart
@@ -0,0 +1,21 @@ +// Copyright (c) 2021, the gRPC project authors. Please see the AUTHORS file +// for details. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +export '../auth/auth.dart' + show HttpBasedAuthenticator, JwtServiceAccountAuthenticator; +export '../client/call.dart' show MetadataProvider, CallOptions; +export '../client/common.dart' show Response, ResponseStream, ResponseFuture; +export 'profiler.dart' show isTimelineLoggingEnabled; +export 'status.dart' show StatusCode, GrpcError;
diff --git a/grpc/lib/src/shared/codec.dart b/grpc/lib/src/shared/codec.dart new file mode 100644 index 0000000..8f771d5 --- /dev/null +++ b/grpc/lib/src/shared/codec.dart
@@ -0,0 +1,68 @@ +// Copyright (c) 2020, the gRPC project authors. Please see the AUTHORS file +// for details. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import 'package:archive/archive.dart'; + +abstract class Codec { + /// Returns the message encoding that this compressor uses. + /// + /// This can be values such as "gzip", "deflate", "snappy", etc. + String get encodingName; + + /// Wraps an existing output stream with a compressed output. + List<int> compress(List<int> data); + + /// Wraps an existing output stream with a uncompressed input data. + List<int> decompress(List<int> data); +} + +/// The "identity", or "none" codec. +/// +/// This codec is special in that it can be used to explicitly disable Call +/// compression on a Channel that by default compresses. +class IdentityCodec implements Codec { + const IdentityCodec(); + + @override + final encodingName = 'identity'; + + @override + List<int> compress(List<int> data) { + return data; + } + + @override + List<int> decompress(List<int> data) { + return data; + } +} + +/// A gzip compressor and decompressor. +class GzipCodec implements Codec { + const GzipCodec(); + + @override + final encodingName = 'gzip'; + + @override + List<int> compress(List<int> data) { + return GZipEncoder().encode(data)!; + } + + @override + List<int> decompress(List<int> data) { + return GZipDecoder().decodeBytes(data); + } +}
diff --git a/grpc/lib/src/shared/codec_registry.dart b/grpc/lib/src/shared/codec_registry.dart new file mode 100644 index 0000000..73c0558 --- /dev/null +++ b/grpc/lib/src/shared/codec_registry.dart
@@ -0,0 +1,47 @@ +// Copyright (c) 2020, the gRPC project authors. Please see the AUTHORS file +// for details. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import 'codec.dart'; + +/// Encloses classes related to the compression and decompression of messages. +class CodecRegistry { + CodecRegistry({List<Codec> codecs = const [IdentityCodec()]}) + : _codecs = {for (var codec in codecs) codec.encodingName: codec}, + _supportedEncodings = codecs.map((c) { + if (c.encodingName.contains(',')) { + throw ArgumentError.value(c.encodingName, 'codecs', + 'contains entries with names containing ","'); + } + return c.encodingName; + }).join(',') { + if (_codecs.length != codecs.length) { + throw ArgumentError.value( + codecs, 'codecs', 'contains multiple entries with the same name'); + } + } + + factory CodecRegistry.empty() => CodecRegistry(codecs: []); + + /// Key refers to the `encodingName` param from the [Codec]. + final Map<String, Codec> _codecs; + + final String _supportedEncodings; + + Codec? lookup(String codecName) { + return _codecs[codecName]; + } + + String get supportedEncodings => _supportedEncodings; +}
diff --git a/grpc/lib/src/shared/io_bits/io_bits.dart b/grpc/lib/src/shared/io_bits/io_bits.dart new file mode 100644 index 0000000..c7409b5 --- /dev/null +++ b/grpc/lib/src/shared/io_bits/io_bits.dart
@@ -0,0 +1,16 @@ +// Copyright (c) 2021, the gRPC project authors. Please see the AUTHORS file +// for details. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +export 'io_bits_io.dart' if (dart.library.html) 'io_bits_web.dart';
diff --git a/grpc/lib/src/shared/io_bits/io_bits_io.dart b/grpc/lib/src/shared/io_bits/io_bits_io.dart new file mode 100644 index 0000000..aae911a --- /dev/null +++ b/grpc/lib/src/shared/io_bits/io_bits_io.dart
@@ -0,0 +1,16 @@ +// Copyright (c) 2021, the gRPC project authors. Please see the AUTHORS file +// for details. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +export 'dart:io' show HttpStatus, X509Certificate;
diff --git a/grpc/lib/src/shared/io_bits/io_bits_web.dart b/grpc/lib/src/shared/io_bits/io_bits_web.dart new file mode 100644 index 0000000..661b3c0 --- /dev/null +++ b/grpc/lib/src/shared/io_bits/io_bits_web.dart
@@ -0,0 +1,20 @@ +// Copyright (c) 2021, the gRPC project authors. Please see the AUTHORS file +// for details. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +export 'dart:html' show HttpStatus; + +/// Should not be used on the Web, but is pulled through [ServiceCall] class +/// which is used in the protoc generated code. +class X509Certificate {}
diff --git a/grpc/lib/src/shared/message.dart b/grpc/lib/src/shared/message.dart new file mode 100644 index 0000000..8b48fb0 --- /dev/null +++ b/grpc/lib/src/shared/message.dart
@@ -0,0 +1,99 @@ +// Copyright (c) 2019, the gRPC project authors. Please see the AUTHORS file +// for details. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import 'dart:async'; +import 'dart:typed_data'; + +import 'codec.dart'; +import 'codec_registry.dart'; +import 'status.dart'; + +abstract class GrpcMessage {} + +class GrpcMetadata extends GrpcMessage { + final Map<String, String> metadata; + GrpcMetadata(this.metadata); + + @override + String toString() => 'gRPC Metadata ($metadata)'; +} + +class GrpcData extends GrpcMessage { + final List<int> data; + final bool isCompressed; + GrpcData(this.data, {required this.isCompressed}) { + ArgumentError.checkNotNull(data); + } + + @override + String toString() => 'gRPC Data (${data.length} bytes)'; +} + +class GrpcMessageSink extends Sink<GrpcMessage> { + late final GrpcMessage message; + bool _messageReceived = false; + + @override + void add(GrpcMessage data) { + if (_messageReceived) { + throw StateError('Too many messages received!'); + } + message = data; + _messageReceived = true; + } + + @override + void close() { + if (!_messageReceived) { + throw StateError('No messages received!'); + } + } +} + +List<int> frame(List<int> rawPayload, [Codec? codec]) { + final compressedPayload = + codec == null ? rawPayload : codec.compress(rawPayload); + final payloadLength = compressedPayload.length; + final bytes = Uint8List(payloadLength + 5); + final header = bytes.buffer.asByteData(0, 5); + header.setUint8(0, codec == null ? 0 : 1); + header.setUint32(1, payloadLength); + bytes.setRange(5, bytes.length, compressedPayload); + return bytes; +} + +StreamTransformer<GrpcMessage, GrpcMessage> grpcDecompressor({ + CodecRegistry? codecRegistry, +}) { + Codec? codec; + return StreamTransformer<GrpcMessage, GrpcMessage>.fromHandlers( + handleData: (GrpcMessage value, EventSink<GrpcMessage> sink) { + if (value is GrpcData && value.isCompressed) { + if (codec == null) { + sink.addError( + GrpcError.unimplemented('Compression mechanism not supported'), + ); + return; + } + sink.add(GrpcData(codec!.decompress(value.data), isCompressed: false)); + return; + } + + if (value is GrpcMetadata && value.metadata.containsKey('grpc-encoding')) { + codec = codecRegistry?.lookup(value.metadata['grpc-encoding']!); + } + sink.add(value); + }); +}
diff --git a/grpc/lib/src/shared/profiler.dart b/grpc/lib/src/shared/profiler.dart new file mode 100644 index 0000000..c229e11 --- /dev/null +++ b/grpc/lib/src/shared/profiler.dart
@@ -0,0 +1,32 @@ +// Copyright (c) 2020, the gRPC project authors. Please see the AUTHORS file +// for details. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import 'dart:developer'; + +typedef TimelineTaskFactory = TimelineTask Function( + {String? filterKey, TimelineTask? parent}); + +TimelineTaskFactory timelineTaskFactory = _defaultTimelineTaskFactory; + +TimelineTask _defaultTimelineTaskFactory( + {String? filterKey, TimelineTask? parent}) => + TimelineTask(filterKey: filterKey, parent: parent); + +const String clientTimelineFilterKey = 'grpc/client'; + +/// Enable logging requests and response for clients. +/// +/// Logging is disabled by default. +bool isTimelineLoggingEnabled = false;
diff --git a/grpc/lib/src/shared/security.dart b/grpc/lib/src/shared/security.dart new file mode 100644 index 0000000..46a11eb --- /dev/null +++ b/grpc/lib/src/shared/security.dart
@@ -0,0 +1,21 @@ +// Copyright (c) 2017, the gRPC project authors. Please see the AUTHORS file +// for details. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import 'dart:io'; + +const supportedAlpnProtocols = ['grpc-exp', 'h2']; + +SecurityContext createSecurityContext(bool isServer) => + SecurityContext()..setAlpnProtocols(supportedAlpnProtocols, isServer);
diff --git a/grpc/lib/src/shared/status.dart b/grpc/lib/src/shared/status.dart new file mode 100644 index 0000000..4e04919 --- /dev/null +++ b/grpc/lib/src/shared/status.dart
@@ -0,0 +1,502 @@ +// Copyright (c) 2017, the gRPC project authors. Please see the AUTHORS file +// for details. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import 'dart:convert'; + +import 'package:meta/meta.dart'; +import 'package:protobuf/protobuf.dart'; + +import '../generated/google/protobuf/any.pb.dart'; +import '../generated/google/rpc/code.pbenum.dart'; +import '../generated/google/rpc/error_details.pb.dart'; +import '../generated/google/rpc/status.pb.dart'; +import 'io_bits/io_bits.dart' show HttpStatus; + +class StatusCode { + /// The operation completed successfully. + static const ok = 0; + + /// The operation was cancelled (typically by the caller). + static const cancelled = 1; + + /// Unknown error. An example of where this error may be returned is if a + /// Status value received from another address space belongs to an error-space + /// that is not known in this address space. Also errors raised by APIs that + /// do not return enough error information may be converted to this error. + static const unknown = 2; + + /// Client specified an invalid argument. Note that this differs from + /// [failedPrecondition]. [invalidArgument] indicates arguments that are + /// problematic regardless of the state of the system (e.g., a malformed file + /// name). + static const invalidArgument = 3; + + /// Deadline expired before operation could complete. For operations that + /// change the state of the system, this error may be returned even if the + /// operation has completed successfully. For example, a successful response + /// from a server could have been delayed long enough for the deadline to + /// expire. + static const deadlineExceeded = 4; + + /// Some requested entity (e.g., file or directory) was not found. + static const notFound = 5; + + /// Some entity that we attempted to create (e.g., file or directory) already + /// exists. + static const alreadyExists = 6; + + /// The caller does not have permission to execute the specified operation. + /// [permissionDenied] must not be used for rejections caused by exhausting + /// some resource (use [resourceExhausted] instead for those errors). + /// [permissionDenied] must not be used if the caller cannot be identified + /// (use [unauthenticated] instead for those errors). + static const permissionDenied = 7; + + /// Some resource has been exhausted, perhaps a per-user quota, or perhaps the + /// entire file system is out of space. + static const resourceExhausted = 8; + + /// Operation was rejected because the system is not in a state required for + /// the operation's execution. For example, directory to be deleted may be + /// non-empty, an rmdir operation is applied to a non-directory, etc. + /// + /// A litmus test that may help a service implementor in deciding between + /// [failedPrecondition], [aborted], and [unavailable]: + /// (a) Use [unavailable] if the client can retry just the failing call. + /// (b) Use [aborted] if the client should retry at a higher-level (e.g., + /// restarting a read-modify-write sequence). + /// (c) Use [failedPrecondition] if the client should not retry until the + /// system state has been explicitly fixed. E.g., if an "rmdir" fails + /// because the directory is non-empty, [failedPrecondition] should be + /// returned since the client should not retry unless they have first + /// fixed up the directory by deleting files from it. + static const failedPrecondition = 9; + + /// The operation was aborted, typically due to a concurrency issue like + /// sequencer check failures, transaction aborts, etc. + /// + /// See litmus test above for deciding between [failedPrecondition], + /// [aborted], and [unavailable]. + static const aborted = 10; + + /// Operation was attempted past the valid range. E.g., seeking or reading + /// past end of file. + /// + /// Unlike invalidArgument, this error indicates a problem that may be fixed + /// if the system state changes. For example, a 32-bit file system will + /// generate invalidArgument if asked to read at an offset that is not in the + /// range [0,2^32-1], but it will generate [outOfRange] if asked to read from + /// an offset past the current file size. + /// + /// There is a fair bit of overlap between [failedPrecondition] and + /// [outOfRange]. We recommend using [outOfRange] (the more specific error) + /// when it applies so that callers who are iterating through a space can + /// easily look for an [outOfRange] error to detect when they are done. + static const outOfRange = 11; + + /// Operation is not implemented or not supported/enabled in this service. + static const unimplemented = 12; + + /// Internal errors. Means some invariants expected by underlying system has + /// been broken. If you see one of these errors, something is very broken. + static const internal = 13; + + /// The service is currently unavailable. This is a most likely a transient + /// condition and may be corrected by retrying with a backoff. + /// + /// See litmus test above for deciding between [failedPrecondition], + /// [aborted], and [unavailable]. + static const unavailable = 14; + + /// Unrecoverable data loss or corruption. + static const dataLoss = 15; + + /// The request does not have valid authentication credentials for the + /// operation. + static const unauthenticated = 16; + + /// Mapping taken from gRPC-Web JS implementation: + /// https://github.com/grpc/grpc-web/blob/master/javascript/net/grpc/web/statuscode.js + static const _httpStatusToGrpcStatus = <int, int>{ + HttpStatus.ok: StatusCode.ok, + HttpStatus.badRequest: StatusCode.invalidArgument, + HttpStatus.unauthorized: StatusCode.unauthenticated, + HttpStatus.forbidden: StatusCode.permissionDenied, + HttpStatus.notFound: StatusCode.notFound, + HttpStatus.conflict: StatusCode.aborted, + HttpStatus.preconditionFailed: StatusCode.failedPrecondition, + HttpStatus.tooManyRequests: StatusCode.resourceExhausted, + HttpStatus.clientClosedRequest: StatusCode.cancelled, + HttpStatus.internalServerError: StatusCode.unknown, + HttpStatus.notImplemented: StatusCode.unimplemented, + HttpStatus.serviceUnavailable: StatusCode.unavailable, + HttpStatus.gatewayTimeout: StatusCode.deadlineExceeded, + }; + + /// Creates a gRPC Status code from a HTTP Status code + static int fromHttpStatus(int status) { + return _httpStatusToGrpcStatus[status] ?? StatusCode.unknown; + } +} + +class GrpcError implements Exception { + final int code; + final String? message; + final Object? rawResponse; + final Map<String, String>? trailers; + final List<GeneratedMessage>? details; + + /// Custom error code. + GrpcError.custom(this.code, + [this.message, this.details, this.rawResponse, this.trailers = const {}]); + + /// The operation completed successfully. + GrpcError.ok([this.message, this.details, this.rawResponse]) + : trailers = const {}, + code = StatusCode.ok; + + /// The operation was cancelled (typically by the caller). + GrpcError.cancelled([this.message, this.details, this.rawResponse]) + : trailers = const {}, + code = StatusCode.cancelled; + + /// Unknown error. An example of where this error may be returned is if a + /// Status value received from another address space belongs to an error-space + /// that is not known in this address space. Also errors raised by APIs that + /// do not return enough error information may be converted to this error. + GrpcError.unknown([this.message, this.details, this.rawResponse]) + : trailers = const {}, + code = StatusCode.unknown; + + /// Client specified an invalid argument. Note that this differs from + /// [failedPrecondition]. [invalidArgument] indicates arguments that are + /// problematic regardless of the state of the system (e.g., a malformed file + /// name). + GrpcError.invalidArgument([this.message, this.details, this.rawResponse]) + : trailers = const {}, + code = StatusCode.invalidArgument; + + /// Deadline expired before operation could complete. For operations that + /// change the state of the system, this error may be returned even if the + /// operation has completed successfully. For example, a successful response + /// from a server could have been delayed long enough for the deadline to + /// expire. + GrpcError.deadlineExceeded([this.message, this.details, this.rawResponse]) + : trailers = const {}, + code = StatusCode.deadlineExceeded; + + /// Some requested entity (e.g., file or directory) was not found. + GrpcError.notFound([this.message, this.details, this.rawResponse]) + : trailers = const {}, + code = StatusCode.notFound; + + /// Some entity that we attempted to create (e.g., file or directory) already + /// exists. + GrpcError.alreadyExists([this.message, this.details, this.rawResponse]) + : trailers = const {}, + code = StatusCode.alreadyExists; + + /// The caller does not have permission to execute the specified operation. + /// [permissionDenied] must not be used for rejections caused by exhausting + /// some resource (use [resourceExhausted] instead for those errors). + /// [permissionDenied] must not be used if the caller cannot be identified + /// (use [unauthenticated] instead for those errors). + GrpcError.permissionDenied([this.message, this.details, this.rawResponse]) + : trailers = const {}, + code = StatusCode.permissionDenied; + + /// Some resource has been exhausted, perhaps a per-user quota, or perhaps the + /// entire file system is out of space. + GrpcError.resourceExhausted([this.message, this.details, this.rawResponse]) + : trailers = const {}, + code = StatusCode.resourceExhausted; + + /// Operation was rejected because the system is not in a state required for + /// the operation's execution. For example, directory to be deleted may be + /// non-empty, an rmdir operation is applied to a non-directory, etc. + /// + /// A litmus test that may help a service implementor in deciding between + /// [failedPrecondition], [aborted], and [unavailable]: + /// (a) Use [unavailable] if the client can retry just the failing call. + /// (b) Use [aborted] if the client should retry at a higher-level (e.g., + /// restarting a read-modify-write sequence). + /// (c) Use [failedPrecondition] if the client should not retry until the + /// system state has been explicitly fixed. E.g., if an "rmdir" fails + /// because the directory is non-empty, [failedPrecondition] should be + /// returned since the client should not retry unless they have first + /// fixed up the directory by deleting files from it. + GrpcError.failedPrecondition([this.message, this.details, this.rawResponse]) + : trailers = const {}, + code = StatusCode.failedPrecondition; + + /// The operation was aborted, typically due to a concurrency issue like + /// sequencer check failures, transaction aborts, etc. + /// + /// See litmus test above for deciding between [failedPrecondition], + /// [aborted], and [unavailable]. + GrpcError.aborted([this.message, this.details, this.rawResponse]) + : trailers = const {}, + code = StatusCode.aborted; + + /// Operation was attempted past the valid range. E.g., seeking or reading + /// past end of file. + /// + /// Unlike invalidArgument, this error indicates a problem that may be fixed + /// if the system state changes. For example, a 32-bit file system will + /// generate invalidArgument if asked to read at an offset that is not in the + /// range [0,2^32-1], but it will generate [outOfRange] if asked to read from + /// an offset past the current file size. + /// + /// There is a fair bit of overlap between [failedPrecondition] and + /// [outOfRange]. We recommend using [outOfRange] (the more specific error) + /// when it applies so that callers who are iterating through a space can + /// easily look for an [outOfRange] error to detect when they are done. + GrpcError.outOfRange([this.message, this.details, this.rawResponse]) + : trailers = const {}, + code = StatusCode.outOfRange; + + /// Operation is not implemented or not supported/enabled in this service. + GrpcError.unimplemented([this.message, this.details, this.rawResponse]) + : trailers = const {}, + code = StatusCode.unimplemented; + + /// Internal errors. Means some invariants expected by underlying system has + /// been broken. If you see one of these errors, something is very broken. + // TODO(sigurdm): This should probably not be an [Exception]. + GrpcError.internal( + [this.message, this.details, this.rawResponse, this.trailers]) + : code = StatusCode.internal; + + /// The service is currently unavailable. This is a most likely a transient + /// condition and may be corrected by retrying with a backoff. + /// + /// See litmus test above for deciding between [failedPrecondition], + /// [aborted], and [unavailable]. + GrpcError.unavailable([this.message, this.details, this.rawResponse]) + : trailers = const {}, + code = StatusCode.unavailable; + + /// Unrecoverable data loss or corruption. + GrpcError.dataLoss([this.message, this.details, this.rawResponse]) + : trailers = const {}, + code = StatusCode.dataLoss; + + /// The request does not have valid authentication credentials for the + /// operation. + GrpcError.unauthenticated([this.message, this.details, this.rawResponse]) + : trailers = const {}, + code = StatusCode.unauthenticated; + + /// Given a status code, return the name + String get codeName => (Code.valueOf(code) ?? Code.UNKNOWN).name; + + @override + bool operator ==(other) { + if (other is! GrpcError) return false; + return code == other.code && message == other.message; + } + + @override + int get hashCode => code.hashCode ^ (message?.hashCode ?? 17); + + @override + String toString() => + 'gRPC Error (code: $code, codeName: $codeName, message: $message, ' + 'details: $details, rawResponse: $rawResponse, trailers: $trailers)'; +} + +/// Parse error details `Any` object into the right kind of `GeneratedMessage`. +/// +/// This list comes from `error_details.proto`. If any new error detail types are +/// added to the protbuf definition, this function should be updated accordingly to +/// support them. +GeneratedMessage parseErrorDetailsFromAny(Any any) { + switch (any.typeUrl) { + case 'type.googleapis.com/google.rpc.RetryInfo': + return RetryInfo.fromBuffer(any.value); + + case 'type.googleapis.com/google.rpc.DebugInfo': + return DebugInfo.fromBuffer(any.value); + + case 'type.googleapis.com/google.rpc.QuotaFailure': + return QuotaFailure.fromBuffer(any.value); + + case 'type.googleapis.com/google.rpc.ErrorInfo': + return ErrorInfo.fromBuffer(any.value); + + case 'type.googleapis.com/google.rpc.PreconditionFailure': + return PreconditionFailure.fromBuffer(any.value); + + case 'type.googleapis.com/google.rpc.BadRequest': + return BadRequest.fromBuffer(any.value); + + case 'type.googleapis.com/google.rpc.RequestInfo': + return RequestInfo.fromBuffer(any.value); + + case 'type.googleapis.com/google.rpc.ResourceInfo': + return ResourceInfo.fromBuffer(any.value); + + case 'type.googleapis.com/google.rpc.Help': + return Help.fromBuffer(any.value); + + case 'type.googleapis.com/google.rpc.LocalizedMessage': + return LocalizedMessage.fromBuffer(any.value); + + default: + return any; + } +} + +/// Validate HTTP status and Content-Type which arrived with the response: +/// reject reponses with non-ok (200) status or unsupported Content-Type. +/// +/// Note that grpc-status arrives in trailers and will be handled by +/// [ClientCall._onResponseData]. +/// +/// gRPC over HTTP2 protocol specification mandates the following: +/// +/// Implementations should expect broken deployments to send non-200 HTTP +/// status codes in responses as well as a variety of non-GRPC content-types +/// and to omit Status & Status-Message. Implementations must synthesize a +/// Status & Status-Message to propagate to the application layer when this +/// occurs. +/// +void validateHttpStatusAndContentType( + int? httpStatus, Map<String, String> headers, + {Object? rawResponse}) { + if (httpStatus == null) { + throw GrpcError.unknown( + 'HTTP response status is unknown', null, rawResponse); + } + + if (httpStatus == 0) { + throw GrpcError.unknown( + 'HTTP request completed without a status (potential CORS issue)', + null, + rawResponse); + } + + final status = StatusCode.fromHttpStatus(httpStatus); + if (status != StatusCode.ok) { + // [httpStatus] itself already indicates an error. Check if we also + // received grpc-status/message (i.e. this is a Trailers-Only response) + // and use this information to report a better error to the application + // layer. However prefer to use status code derived from HTTP status + // if grpc-status itself does not provide an informative error. + final error = grpcErrorDetailsFromTrailers(headers); + if (error == null || error.code == StatusCode.unknown) { + throw GrpcError.custom( + status, + error?.message ?? + 'HTTP connection completed with ${httpStatus} instead of 200', + error?.details, + rawResponse, + error?.trailers ?? toCustomTrailers(headers), + ); + } + throw error; + } + + final contentType = headers['content-type']; + if (contentType == null) { + throw GrpcError.unknown('missing content-type header', null, rawResponse); + } + + // Check if content-type header indicates a supported format. + if (!_validContentTypePrefix.any(contentType.startsWith)) { + throw GrpcError.unknown( + 'unsupported content-type (${contentType})', null, rawResponse); + } +} + +GrpcError? grpcErrorDetailsFromTrailers(Map<String, String> trailers) { + final status = trailers['grpc-status']; + final statusCode = status != null ? int.parse(status) : StatusCode.unknown; + + if (statusCode != StatusCode.ok) { + final message = _tryDecodeStatusMessage(trailers['grpc-message']); + final statusDetails = trailers[_statusDetailsHeader]; + return GrpcError.custom( + statusCode, + message, + statusDetails == null + ? const <GeneratedMessage>[] + : decodeStatusDetails(statusDetails), + null, + toCustomTrailers(trailers), + ); + } + + return null; +} + +Map<String, String> toCustomTrailers(Map<String, String> trailers) { + return Map.from(trailers) + ..remove(':status') + ..remove('content-type') + ..remove('grpc-status') + ..remove('grpc-message'); +} + +const _statusDetailsHeader = 'grpc-status-details-bin'; + +/// All accepted content-type header's prefix. We are being more permissive +/// then gRPC and gRPC-Web specifications because some of the services +/// return slightly different content-types. +const _validContentTypePrefix = [ + 'application/grpc', + 'application/json+protobuf', + 'application/x-protobuf' +]; + +/// Given a string of base64url data, attempt to parse a Status object from it. +/// Once parsed, it will then map each detail item and attempt to parse it into +/// its respective GeneratedMessage type, returning the list of parsed detail items +/// as a `List<GeneratedMessage>`. +/// +/// Prior to creating the Status object we pad the data to ensure its length is +/// an even multiple of 4, which is a requirement in Dart when decoding base64url data. +/// +/// If any errors are thrown during decoding/parsing, it will return an empty list. +@visibleForTesting +List<GeneratedMessage> decodeStatusDetails(String data) { + try { + final parsedStatus = Status.fromBuffer( + base64Url.decode(data.padRight((data.length + 3) & ~3, '='))); + return parsedStatus.details.map(parseErrorDetailsFromAny).toList(); + } catch (e) { + return <GeneratedMessage>[]; + } +} + +/// Decode percent encoded status message contained in 'grpc-message' trailer. +String? _tryDecodeStatusMessage(String? statusMessage) { + if (statusMessage == null) { + return statusMessage; + } + + try { + return Uri.decodeFull(statusMessage); + } catch (_) { + // gRPC over HTTP2 protocol specification mandates: + // + // When decoding invalid values, implementations MUST NOT error or throw + // away the message. At worst, the implementation can abort decoding the + // status message altogether such that the user would received the raw + // percent-encoded form. + // + return statusMessage; + } +}
diff --git a/grpc/lib/src/shared/streams.dart b/grpc/lib/src/shared/streams.dart new file mode 100644 index 0000000..6a8e3b7 --- /dev/null +++ b/grpc/lib/src/shared/streams.dart
@@ -0,0 +1,166 @@ +// Copyright (c) 2017, the gRPC project authors. Please see the AUTHORS file +// for details. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import 'dart:convert'; +import 'dart:math'; +import 'dart:typed_data'; + +import 'package:http2/transport.dart'; + +import 'message.dart'; +import 'status.dart'; + +class GrpcHttpEncoder extends Converter<GrpcMessage, StreamMessage> { + @override + StreamMessage convert(GrpcMessage input) { + if (input is GrpcMetadata) { + final headers = <Header>[]; + input.metadata.forEach((key, value) { + headers.add(Header(ascii.encode(key), utf8.encode(value))); + }); + return HeadersStreamMessage(headers); + } else if (input is GrpcData) { + return DataStreamMessage(frame(input.data)); + } + throw GrpcError.internal('Unexpected message type'); + } +} + +class GrpcHttpDecoder extends Converter<StreamMessage, GrpcMessage> { + /// [true] if this decoder is used for decoding responses. + final bool forResponse; + + GrpcHttpDecoder({this.forResponse = false}); + + @override + GrpcMessage convert(StreamMessage input) { + final sink = GrpcMessageSink(); + startChunkedConversion(sink) + ..add(input) + ..close(); + return sink.message; + } + + @override + Sink<StreamMessage> startChunkedConversion(Sink<GrpcMessage> sink) { + return _GrpcMessageConversionSink(sink, forResponse); + } +} + +class _GrpcMessageConversionSink extends ChunkedConversionSink<StreamMessage> { + final Sink<GrpcMessage> _out; + final bool _forResponse; + + final _dataHeader = Uint8List(5); + Uint8List? _data; + int _dataOffset = 0; + + bool _headersReceived = false; + + _GrpcMessageConversionSink(this._out, this._forResponse); + + void _addData(DataStreamMessage chunk) { + final chunkData = chunk.bytes; + final chunkLength = chunkData.length; + var chunkReadOffset = 0; + + while (chunkReadOffset < chunkLength) { + if (_data == null) { + // Reading header. + final headerRemaining = _dataHeader.lengthInBytes - _dataOffset; + final chunkRemaining = chunkLength - chunkReadOffset; + final toCopy = min(headerRemaining, chunkRemaining); + _dataHeader.setRange( + _dataOffset, _dataOffset + toCopy, chunkData, chunkReadOffset); + _dataOffset += toCopy; + chunkReadOffset += toCopy; + if (_dataOffset == _dataHeader.lengthInBytes) { + final dataLength = _dataHeader.buffer.asByteData().getUint32(1); + // TODO(jakobr): Sanity check dataLength. Max size? + _data = Uint8List(dataLength); + _dataOffset = 0; + } + } + if (_data != null) { + // Reading data. + final dataRemaining = _data!.lengthInBytes - _dataOffset; + if (dataRemaining > 0) { + final chunkRemaining = chunkLength - chunkReadOffset; + final toCopy = min(dataRemaining, chunkRemaining); + _data!.setRange( + _dataOffset, _dataOffset + toCopy, chunkData, chunkReadOffset); + _dataOffset += toCopy; + chunkReadOffset += toCopy; + } + if (_dataOffset == _data!.lengthInBytes) { + _out.add(GrpcData(_data!, + isCompressed: _dataHeader.buffer.asByteData().getUint8(0) != 0)); + _data = null; + _dataOffset = 0; + } + } + } + } + + void _addHeaders(HeadersStreamMessage chunk) { + if (_data != null || _dataOffset != 0) { + // We were in the middle of receiving data, so receiving a header frame + // is a violation of the gRPC protocol. + throw GrpcError.unimplemented('Received header while reading data'); + } + final headers = <String, String>{}; + for (var header in chunk.headers) { + // TODO(jakobr): Handle duplicate header names correctly. + headers[ascii.decode(header.name)] = ascii.decode(header.value); + } + if (!_headersReceived) { + if (_forResponse) { + // Validate :status and content-type header here synchronously before + // attempting to parse subsequent DataStreamMessage. + final httpStatus = headers.containsKey(':status') + ? int.tryParse(headers[':status']!) + : null; + + // Validation might throw an exception. When [GrpcHttpDecoder] is + // used as a [StreamTransformer] the underlying implementation of + // [StreamTransformer.bind] will take care of forwarding this + // exception into the stream as an error. + validateHttpStatusAndContentType(httpStatus, headers); + } + _headersReceived = true; + } + _out.add(GrpcMetadata(headers)); + } + + @override + void add(StreamMessage chunk) { + if (chunk is DataStreamMessage) { + _addData(chunk); + } else if (chunk is HeadersStreamMessage) { + _addHeaders(chunk); + } else { + // No clue what this is. + throw GrpcError.unimplemented('Received unknown HTTP/2 frame type'); + } + } + + @override + void close() { + if (_data != null || _dataOffset != 0) { + throw GrpcError.unavailable('Closed in non-idle state'); + } + _out.close(); + } +}
diff --git a/grpc/lib/src/shared/timeout.dart b/grpc/lib/src/shared/timeout.dart new file mode 100644 index 0000000..c00ee9b --- /dev/null +++ b/grpc/lib/src/shared/timeout.dart
@@ -0,0 +1,61 @@ +// Copyright (c) 2017, the gRPC project authors. Please see the AUTHORS file +// for details. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +/// Convert [timeout] to grpc-timeout header string format. +// Mostly inspired by grpc-java implementation. +// TODO(jakobr): Modify to match grpc/core implementation instead. +String toTimeoutString(Duration duration) { + const cutoff = 100000; + final timeout = duration.inMicroseconds; + if (timeout < 0) { + // Smallest possible timeout. + return '1n'; + } else if (timeout < cutoff) { + return '${timeout}u'; + } else if (timeout < cutoff * 1000) { + return '${timeout ~/ 1000}m'; + } else if (timeout < cutoff * 1000 * 1000) { + return '${timeout ~/ 1000000}S'; + } else if (timeout < cutoff * 1000 * 1000 * 60) { + return '${timeout ~/ 60000000}M'; + } else { + return '${timeout ~/ 3600000000}H'; + } +} + +/// Convert [timeout] from grpc-timeout header string format to [Duration]. +/// Returns [null] if [timeout] is not correctly formatted. +Duration? fromTimeoutString(String? timeout) { + if (timeout == null) return null; + if (timeout.length < 2) return null; + final value = int.tryParse(timeout.substring(0, timeout.length - 1)); + if (value == null) return null; + switch (timeout[timeout.length - 1]) { + case 'n': + return Duration(microseconds: value * 1000); + case 'u': + return Duration(microseconds: value); + case 'm': + return Duration(milliseconds: value); + case 'S': + return Duration(seconds: value); + case 'M': + return Duration(minutes: value); + case 'H': + return Duration(hours: value); + default: + return null; + } +}
diff --git a/grpc/pubspec.yaml b/grpc/pubspec.yaml new file mode 100644 index 0000000..b4b9a6c --- /dev/null +++ b/grpc/pubspec.yaml
@@ -0,0 +1,29 @@ +name: grpc +description: Dart implementation of gRPC, a high performance, open-source universal RPC framework. + +version: 3.0.2 + +repository: https://github.com/grpc/grpc-dart + +environment: + sdk: '>=2.12.0 <3.0.0' + +dependencies: + archive: ^3.0.0 + async: ^2.5.0 + crypto: ^3.0.0 + fixnum: ^1.0.0 + googleapis_auth: ^1.1.0 + meta: ^1.3.0 + http: ^0.13.0 + http2: ^2.0.0 + protobuf: ^2.0.0 + +dev_dependencies: + build_runner: ^2.0.0 + build_test: ^2.0.0 + mockito: ^5.0.0 + path: ^1.8.0 + test: ^1.16.0 + stream_channel: ^2.1.0 + stream_transform: ^2.0.0
diff --git a/grpc/tool/regenerate.sh b/grpc/tool/regenerate.sh new file mode 100755 index 0000000..51b4e5f --- /dev/null +++ b/grpc/tool/regenerate.sh
@@ -0,0 +1,24 @@ +#!/usr/bin/env bash + +set -ex + +mkdir -p lib/src/generated +protoc --dart_out=grpc:lib/src/generated --proto_path lib/src/protos $(find lib/src/protos -iname "*.proto") +rm -f lib/src/generated/*.pbjson.dart +rm -f lib/src/generated/{empty,test}.pbenum.dart +dart format lib/src/generated + +protoc --dart_out=grpc:test/src/generated --proto_path test/src/protos test/src/protos/echo.proto +dart format test/src/generated + +for dir in interop example/*/; do + pushd $dir + echo [Regenerating in $dir] + if [[ -x "tool/regenerate.sh" ]]; then + tool/regenerate.sh + elif [[ -d "protos" ]]; then + protoc --dart_out=grpc:lib/src/generated --proto_path protos $(find protos -iname "*.proto") + dart format lib/src/generated + fi + popd +done \ No newline at end of file
diff --git a/http2/.github/workflows/test-package.yml b/http2/.github/workflows/test-package.yml new file mode 100644 index 0000000..21a3c50 --- /dev/null +++ b/http2/.github/workflows/test-package.yml
@@ -0,0 +1,61 @@ +name: Dart CI + +on: + # Run on PRs and pushes to the default branch. + push: + branches: [ master ] + pull_request: + branches: [ master ] + schedule: + - cron: "0 0 * * 0" + +env: + PUB_ENVIRONMENT: bot.github + +jobs: + # Check code formatting and static analysis on a single OS (linux) + # against Dart dev. + analyze: + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + sdk: [dev] + steps: + - uses: actions/checkout@v2 + - uses: dart-lang/setup-dart@v0.3 + with: + sdk: ${{ matrix.sdk }} + - id: install + name: Install dependencies + run: dart pub get + - name: Check formatting + run: dart format --output=none --set-exit-if-changed . + if: always() && steps.install.outcome == 'success' + - name: Analyze code + run: dart analyze --fatal-infos + if: always() && steps.install.outcome == 'success' + + # Run tests on a matrix consisting of two dimensions: + # 1. OS: ubuntu-latest, (macos-latest, windows-latest) + # 2. release channel: dev + test: + needs: analyze + runs-on: ${{ matrix.os }} + strategy: + fail-fast: false + matrix: + # Add macos-latest and/or windows-latest if relevant for this package. + os: [ubuntu-latest] + sdk: [dev] + steps: + - uses: actions/checkout@v2 + - uses: dart-lang/setup-dart@v0.3 + with: + sdk: ${{ matrix.sdk }} + - id: install + name: Install dependencies + run: dart pub get + - name: Run VM tests + run: dart test --platform vm + if: always() && steps.install.outcome == 'success'
diff --git a/http2/.gitignore b/http2/.gitignore new file mode 100644 index 0000000..ac98e87 --- /dev/null +++ b/http2/.gitignore
@@ -0,0 +1,4 @@ +# Don’t commit the following directories created by pub. +.dart_tool +.packages +pubspec.lock
diff --git a/http2/.test_config b/http2/.test_config new file mode 100644 index 0000000..2fa4b96 --- /dev/null +++ b/http2/.test_config
@@ -0,0 +1,5 @@ +{ + "test_package": { + "platforms" : ["vm"] + } +}
diff --git a/http2/AUTHORS b/http2/AUTHORS new file mode 100644 index 0000000..93b7228 --- /dev/null +++ b/http2/AUTHORS
@@ -0,0 +1,8 @@ +# Below is a list of people and organizations that have contributed +# to the project. Names should be added to the list like so: +# +# Name/Organization <email address> + +Google Inc. <*@google.com> + +Alexandre Ardhuin <alexandre.ardhuin@gmail.com>
diff --git a/http2/BUILD.gn b/http2/BUILD.gn new file mode 100644 index 0000000..b9a679c --- /dev/null +++ b/http2/BUILD.gn
@@ -0,0 +1,46 @@ +# This file is generated by package_importer.py for http2-2.0.0 + +import("//build/dart/dart_library.gni") + +dart_library("http2") { + package_name = "http2" + + language_version = "2.12" + + disable_analysis = true + + deps = [ + ] + + sources = [ + "http2.dart", + "multiprotocol_server.dart", + "src/artificial_server_socket.dart", + "src/async_utils/async_utils.dart", + "src/byte_utils.dart", + "src/connection.dart", + "src/connection_preface.dart", + "src/error_handler.dart", + "src/flowcontrol/connection_queues.dart", + "src/flowcontrol/queue_messages.dart", + "src/flowcontrol/stream_queues.dart", + "src/flowcontrol/window.dart", + "src/flowcontrol/window_handler.dart", + "src/frames/frame_defragmenter.dart", + "src/frames/frame_reader.dart", + "src/frames/frame_types.dart", + "src/frames/frame_utils.dart", + "src/frames/frame_writer.dart", + "src/frames/frames.dart", + "src/hpack/hpack.dart", + "src/hpack/huffman.dart", + "src/hpack/huffman_table.dart", + "src/ping/ping_handler.dart", + "src/settings/settings.dart", + "src/streams/stream_handler.dart", + "src/sync_errors.dart", + "src/testing/client.dart", + "src/testing/debug.dart", + "transport.dart", + ] +}
diff --git a/http2/CHANGELOG.md b/http2/CHANGELOG.md new file mode 100644 index 0000000..7897c34 --- /dev/null +++ b/http2/CHANGELOG.md
@@ -0,0 +1,89 @@ +## 2.0.0 + +* Migrate to null safety. + +## 1.0.1 + +* Add `TransportConnection.onInitialPeerSettingsReceived` which fires when + initial SETTINGS frame is received from the peer. + +## 1.0.0 + +* Graduate package to 1.0. +* `package:http2/http2.dart` now reexports `package:http2/transport.dart`. + +## 0.1.9 + +* Discard messages incoming after stream cancellation. + +## 0.1.8+2 + +* On connection termination, try to dispatch existing messages, thereby avoiding + terminating existing streams. + +* Fix `ClientTransportConnection.isOpen` to return `false` if we have exhausted + the number of max-concurrent-streams. + +## 0.1.8+1 + +* Switch all uppercase constants from `dart:convert` to lowercase. + +## 0.1.8 + +* More changes required for making tests pass under Dart 2.0 runtime. +* Modify sdk constraint to require '>=2.0.0-dev.40.0'. + +## 0.1.7 + +* Fixes for Dart 2.0. + +## 0.1.6 + +* Strong mode fixes and other cleanup. + +## 0.1.5 + +* Removed use of new `Function` syntax, since it isn't fully supported in Dart + 1.24. + +## 0.1.4 + +* Added an `onActiveStateChanged` callback to `Connection`, which is invoked when + the connection changes state from idle to active or from active to idle. This + can be used to implement an idle connection timeout. + +## 0.1.3 + +* Fixed a bug where a closed window would not open correctly due to an increase + in initial window size. + +## 0.1.2 + +* The endStream bit is now set on the requested frame, instead of on an empty + data frame following it. +* Added an `onTerminated` hook that is called when a TransportStream receives + a RST_STREAM frame. + +## 0.1.1+2 + +* Add errorCode to exception toString message. + +## 0.1.1+1 + +* Fixing a performance issue in case the underlying socket is not writeable +* Allow clients of MultiProtocolHttpServer to supply [http.ServerSettings] +* Allow the draft version 'h2-14' in the ALPN protocol negogiation. + +## 0.1.1 + +* Adding support for MultiProtocolHttpServer in the + `package:http2/multiprotocol_server.dart` library + +## 0.1.0 + +* First version of a HTTP/2 transport implementation in the + `package:http2/transport.dart` library + +## 0.0.1 + +- Initial version
diff --git a/http2/CONTRIBUTING.md b/http2/CONTRIBUTING.md new file mode 100644 index 0000000..6f5e0ea --- /dev/null +++ b/http2/CONTRIBUTING.md
@@ -0,0 +1,33 @@ +Want to contribute? Great! First, read this page (including the small print at +the end). + +### Before you contribute +Before we can use your code, you must sign the +[Google Individual Contributor License Agreement](https://cla.developers.google.com/about/google-individual) +(CLA), which you can do online. The CLA is necessary mainly because you own the +copyright to your changes, even after your contribution becomes part of our +codebase, so we need your permission to use and distribute your code. We also +need to be sure of various other things—for instance that you'll tell us if you +know that your code infringes on other people's patents. You don't have to sign +the CLA until after you've submitted your code for review and a member has +approved it, but you must do it before we can put your code into our codebase. + +Before you start working on a larger contribution, you should get in touch with +us first through the issue tracker with your idea so that we can help out and +possibly guide you. Coordinating up front makes it much easier to avoid +frustration later on. + +### Code reviews +All submissions, including submissions by project members, require review. + +### File headers +All files in the project must start with the following header. + + // 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. + +### The small print +Contributions made by corporations are covered by a different agreement than the +one above, the +[Software Grant and Corporate Contributor License Agreement](https://developers.google.com/open-source/cla/corporate).
diff --git a/http2/LICENSE b/http2/LICENSE new file mode 100644 index 0000000..de31e1a --- /dev/null +++ b/http2/LICENSE
@@ -0,0 +1,26 @@ +Copyright 2015, the Dart project authors. All rights reserved. +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 Inc. 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/http2/README.md b/http2/README.md new file mode 100644 index 0000000..4bea5db --- /dev/null +++ b/http2/README.md
@@ -0,0 +1,62 @@ +# HTTP/2 for Dart + +This library provides an http/2 interface on top of a bidirectional stream of bytes. + +## Usage: + +Here is a minimal example of connecting to a http/2 capable server, requesting a resource and +iterating over the response. + +```dart +import 'dart:convert'; +import 'dart:io'; + +import 'package:http2/http2.dart'; + +main() async { + var uri = Uri.parse('https://www.google.com/'); + + var transport = new ClientTransportConnection.viaSocket( + await SecureSocket.connect( + uri.host, + uri.port, + supportedProtocols: ['h2'], + ), + ); + + var stream = transport.makeRequest( + [ + new Header.ascii(':method', 'GET'), + new Header.ascii(':path', uri.path), + new Header.ascii(':scheme', uri.scheme), + new Header.ascii(':authority', uri.host), + ], + endStream: true, + ); + + await for (var message in stream.incomingMessages) { + if (message is HeadersStreamMessage) { + for (var header in message.headers) { + var name = utf8.decode(header.name); + var value = utf8.decode(header.value); + print('Header: $name: $value'); + } + } else if (message is DataStreamMessage) { + // Use [message.bytes] (but respect 'content-encoding' header) + } + } + await transport.finish(); +} +``` + +An example with better error handling is available [here][example]. + +See the [API docs][api] for more details. + +## Features and bugs + +Please file feature requests and bugs at the [issue tracker][tracker]. + +[tracker]: https://github.com/dart-lang/http2/issues +[api]: https://pub.dev/documentation/http2/latest/ +[example]: https://github.com/dart-lang/http2/blob/master/example/display_headers.dart
diff --git a/http2/analysis_options.yaml b/http2/analysis_options.yaml new file mode 100644 index 0000000..2ad0cbc --- /dev/null +++ b/http2/analysis_options.yaml
@@ -0,0 +1,34 @@ +include: package:pedantic/analysis_options.yaml + +analyzer: + strong-mode: + implicit-casts: false + errors: + unused_element: error + unused_import: error + unused_local_variable: error + dead_code: error + +linter: + rules: + - avoid_unused_constructor_parameters + - await_only_futures + - camel_case_types + - comment_references + - control_flow_in_finally + - directives_ordering + - empty_statements + - hash_and_equals + - implementation_imports + - iterable_contains_unrelated_type + - list_remove_unrelated_type + - non_constant_identifier_names + - only_throw_errors + - overridden_fields + - package_api_docs + - package_names + - package_prefixed_library_names + - test_types_in_equals + - throw_in_finally + - unnecessary_parenthesis + - unnecessary_brace_in_string_interps
diff --git a/http2/dart_test.yaml b/http2/dart_test.yaml new file mode 100644 index 0000000..7bcbfc9 --- /dev/null +++ b/http2/dart_test.yaml
@@ -0,0 +1,2 @@ +tags: + flaky: # Tests that should be run as a separate job on Travis
diff --git a/http2/example/display_headers.dart b/http2/example/display_headers.dart new file mode 100644 index 0000000..9cefdab --- /dev/null +++ b/http2/example/display_headers.dart
@@ -0,0 +1,63 @@ +import 'dart:async'; +import 'dart:convert'; +import 'dart:io'; + +import 'package:http2/transport.dart'; + +void main(List<String> args) async { + if (args.length != 1) { + print('Usage: dart display_headers.dart <HTTPS_URI>'); + exit(1); + } + + var uriArg = args[0]; + + if (!uriArg.startsWith('https://')) { + print('URI must start with https://'); + exit(1); + } + + var uri = Uri.parse(uriArg); + + var socket = await connect(uri); + + // The default client settings will disable server pushes. We + // therefore do not need to deal with [stream.peerPushes]. + var transport = ClientTransportConnection.viaSocket(socket); + + var headers = [ + Header.ascii(':method', 'GET'), + Header.ascii(':path', uri.path), + Header.ascii(':scheme', uri.scheme), + Header.ascii(':authority', uri.host), + ]; + + var stream = transport.makeRequest(headers, endStream: true); + await for (var message in stream.incomingMessages) { + if (message is HeadersStreamMessage) { + for (var header in message.headers) { + var name = utf8.decode(header.name); + var value = utf8.decode(header.value); + print('$name: $value'); + } + } else if (message is DataStreamMessage) { + // Use [message.bytes] (but respect 'content-encoding' header) + } + } + await transport.finish(); +} + +Future<Socket> connect(Uri uri) async { + var useSSL = uri.scheme == 'https'; + if (useSSL) { + var secureSocket = await SecureSocket.connect(uri.host, uri.port, + supportedProtocols: ['h2']); + if (secureSocket.selectedProtocol != 'h2') { + throw Exception('Failed to negogiate http/2 via alpn. Maybe server ' + "doesn't support http/2."); + } + return secureSocket; + } else { + return await Socket.connect(uri.host, uri.port); + } +}
diff --git a/http2/experimental/server.dart b/http2/experimental/server.dart new file mode 100644 index 0000000..e85d372 --- /dev/null +++ b/http2/experimental/server.dart
@@ -0,0 +1,165 @@ +// 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:convert'; +import 'dart:io'; + +import 'package:http2/src/testing/debug.dart' hide print; +import 'package:http2/transport.dart'; +import 'package:pedantic/pedantic.dart'; + +const bool DEBUGGING = false; + +const String HOSTNAME = 'localhost'; +const int PORT = 7777; + +void main() async { + String localFile(String path) => Platform.script.resolve(path).toFilePath(); + + var context = SecurityContext() + ..usePrivateKey(localFile('server_key.pem'), password: 'dartdart') + ..useCertificateChain(localFile('server_chain.pem')) + ..setAlpnProtocols(['h2'], true); + + var server = await SecureServerSocket.bind(HOSTNAME, PORT, context); + print('HTTP/2 server listening on https://$HOSTNAME:$PORT'); + + runZonedGuarded(() { + server.listen(handleClient); + }, (e, s) { + print('Unexpected error: $e'); + print('Unexpected error - stack: $s'); + }); +} + +void handleClient(SecureSocket socket) { + dumpInfo('main', 'Got new https client'); + + var connection; + if (DEBUGGING) { + connection = debugPrintingConnection(socket); + } else { + connection = ServerTransportConnection.viaSocket(socket); + } + + connection.incomingStreams.listen((ServerTransportStream stream) async { + dumpInfo('main', 'Got new HTTP/2 stream with id: ${stream.id}'); + + var pathSeen = false; + unawaited(stream.incomingMessages.forEach((StreamMessage msg) async { + dumpInfo('${stream.id}', 'Got new incoming message'); + if (msg is HeadersStreamMessage) { + dumpHeaders('${stream.id}', msg.headers); + if (!pathSeen) { + var path = pathFromHeaders(msg.headers); + pathSeen = true; + if (path == '/') { + unawaited(sendHtml(stream)); + } else if (path == '/iframe' || path == '/iframe2') { + unawaited(sendIFrameHtml(stream, path)); + } else { + unawaited(send404(stream, path)); + } + } + } else if (msg is DataStreamMessage) { + dumpData('${stream.id}', msg.bytes); + } + })); + }); +} + +void dumpHeaders(String prefix, List<Header> headers) { + for (var i = 0; i < headers.length; i++) { + var key = ascii.decode(headers[i].name); + var value = ascii.decode(headers[i].value); + print('[$prefix] $key: $value'); + } +} + +String pathFromHeaders(List<Header> headers) { + for (var i = 0; i < headers.length; i++) { + if (ascii.decode(headers[i].name) == ':path') { + return ascii.decode(headers[i].value); + } + } + throw Exception('Expected a :path header, but did not find one.'); +} + +void dumpData(String prefix, List<int> data) { + print('[$prefix] Got ${data.length} bytes.'); +} + +void dumpInfo(String prefix, String msg) { + print('[$prefix] $msg'); +} + +Future sendHtml(ServerTransportStream stream) async { + unawaited(push(stream, '/iframe', sendIFrameHtml)); + unawaited(push(stream, '/iframe2', sendIFrameHtml)); + unawaited(push(stream, '/favicon.ico', send404)); + + stream.sendHeaders([ + Header.ascii(':status', '200'), + Header.ascii('content-type', 'text/html; charset=utf-8'), + ]); + stream.sendData(ascii.encode(''' +<html> + <head><title>hello</title></head> + <body> + <h1> head </h1> + first <br /> + <iframe src='/iframe' with="100" height="100"></iframe> <br /> + second <br /> + <iframe src='/iframe2' with="100" height="100"></iframe> <br /> + </body> +</html> +''')); + return stream.outgoingMessages.close(); +} + +Future push(ServerTransportStream stream, String path, + Future Function(TransportStream, String path) sendResponse) async { + var requestHeaders = [ + Header.ascii(':authority', '$HOSTNAME:$PORT'), + Header.ascii(':method', 'GET'), + Header.ascii(':path', path), + Header.ascii(':scheme', 'https'), + ]; + + var pushStream = stream.push(requestHeaders); + await sendResponse(pushStream, path); +} + +Future sendIFrameHtml(TransportStream stream, String path) async { + stream.sendHeaders([ + Header.ascii(':status', '200'), + Header.ascii('content-type', 'text/html; charset=utf-8'), + ]); + stream.sendData(ascii.encode(''' +<html> + <head><title>Content for '$path' inside an IFrame.</title></head> + <body> + <h2>Content for '$path' inside an IFrame.</h2> + </body> +</html> +''')); + await stream.outgoingMessages.close(); +} + +Future send404(TransportStream stream, String path) async { + stream.sendHeaders([ + Header.ascii(':status', '404'), + Header.ascii('content-type', 'text/html; charset=utf-8'), + ]); + stream.sendData(ascii.encode(''' +<html> + <head><title>Path '$path' was not found on this server.</title></head> + <body> + <h1>Path '$path' was not found on this server.</h1> + </body> +</html> +''')); + return stream.outgoingMessages.close(); +}
diff --git a/http2/experimental/server_chain.pem b/http2/experimental/server_chain.pem new file mode 100644 index 0000000..4163fe7 --- /dev/null +++ b/http2/experimental/server_chain.pem
@@ -0,0 +1,57 @@ +-----BEGIN CERTIFICATE----- +MIIDKTCCAhGgAwIBAgIJAOWmjTS+OnTEMA0GCSqGSIb3DQEBCwUAMBcxFTATBgNV +BAMMDGludGVybWVkaWF0ZTAeFw0xNTA1MTgwOTAwNDBaFw0yMzA4MDQwOTAwNDBa +MBQxEjAQBgNVBAMMCWxvY2FsaG9zdDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCC +AQoCggEBALlcwQJuzd+xH8QFgfJSn5tRlvhkldSX98cE7NiA602NBbnAVyUrkRXq +Ni75lgt0kwjYfA9z674m8WSVbgpLPintPCla9CYky1TH0keIs8Rz6cGWHryWEHiu +EDuljQynu2b3sAFuHu9nfWurbJwZnFakBKpdQ9m4EyOZCHC/jHYY7HacKSXg1Cki +we2ca0BWDrcqy8kLy0dZ5oC6IZG8O8drAK8f3f44CRYw59D3sOKBrKXaabpvyEcb +N7Wk2HDBVwHpUJo1reVwtbM8dhqQayYSD8oXnGpP3RQNu/e2rzlXRyq/BfcDY1JI +7TbC4t/7/N4EcPSpGsTcSOC9A7FpzvECAwEAAaN7MHkwCQYDVR0TBAIwADAsBglg +hkgBhvhCAQ0EHxYdT3BlblNTTCBHZW5lcmF0ZWQgQ2VydGlmaWNhdGUwHQYDVR0O +BBYEFCnwiEMMFZh7NhCr+qA8K0w4Q+AOMB8GA1UdIwQYMBaAFB0h1Evsaw2vfrmS +YuoCTmC4EE6ZMA0GCSqGSIb3DQEBCwUAA4IBAQAcFmHMaXRxyoNaeOowQ6iQWoZd +AUbvG7SHr7I6Pi2aqdqofsKWts7Ytm5WsS0M2nN+sW504houu0iCPeJJX8RQw2q4 +CCcNOs9IXk+2uMzlpocHpv+yYoUiD5DxgWh7eghQMLyMpf8FX3Gy4VazeuXznHOM +4gE4L417xkDzYOzqVTp0FTyAPUv6G2euhNCD6TMru9REcRhYul+K9kocjA5tt2KG +MH6y28LXbLyq4YJUxSUU9gY/xlnbbZS48KDqEcdYC9zjW9nQ0qS+XQuQuFIcwjJ5 +V4kAUYxDu6FoTpyQjgsrmBbZlKNxH7Nj4NDlcdJhp/zeSKHqWa5hSWjjKIxp +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIDAjCCAeqgAwIBAgIJAOWmjTS+OnTDMA0GCSqGSIb3DQEBCwUAMBgxFjAUBgNV +BAMMDXJvb3RhdXRob3JpdHkwHhcNMTUwNTE4MDkwMDQwWhcNMjMwODA0MDkwMDQw +WjAXMRUwEwYDVQQDDAxpbnRlcm1lZGlhdGUwggEiMA0GCSqGSIb3DQEBAQUAA4IB +DwAwggEKAoIBAQDSrAO1CoPvUllgLOzDm5nG0skDF7vh1DUgAIDVGz0ecD0JFbQx +EF79pju/6MbtpTW2FYvRp11t/G7rGtX923ybOHY/1MNFQrdIvPlO1VV7IGKjoMwP +DNeb0fIGjHoE9QxaDxR8NX8xQbItpsw+TUtRfc9SLkR+jaYJfVRoM21BOncZbSHE +YKiZlEbpecB/+EtwVpgvl+8mPD5U07Fi4fp/lza3WXInXQPyiTVllIEJCt4PKmlu +MocNaJOW38bysL7i0PzDpVZtOxLHOTaW68yF3FckIHNCaA7k1ABEEEegjFMmIao7 +B9w7A0jvr4jZVvNmui5Djjn+oJxwEVVgyf8LAgMBAAGjUDBOMB0GA1UdDgQWBBQd +IdRL7GsNr365kmLqAk5guBBOmTAfBgNVHSMEGDAWgBRk81s9d0ZbiZhh44KckwPb +oTc0XzAMBgNVHRMEBTADAQH/MA0GCSqGSIb3DQEBCwUAA4IBAQBZQTK0plfdB5PC +cC5icut4EmrByJa1RbU7ayuEE70e7hla6KVmVjVdCBGltI4jBYwfhKbRItHiAJ/8 +x+XZKBG8DLPFuDb7lAa1ObhAYF7YThUFPQYaBhfzKcWrdmWDBFpvNv6E0Mm364dZ +e7Yxmbe5S4agkYPoxEzgEYmcUk9jbjdR6eTbs8laG169ljrECXfEU9RiAcqz5iSX +NLSewqB47hn3B9qgKcQn+PsgO2j7M+rfklhNgeGJeWmy7j6clSOuCsIjWHU0RLQ4 +0W3SB/rpEAJ7fgQbYUPTIUNALSOWi/o1tDX2mXPRjBoxqAv7I+vYk1lZPmSzkyRh +FKvRDxsW +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIDAzCCAeugAwIBAgIJAJ0MomS4Ck+8MA0GCSqGSIb3DQEBCwUAMBgxFjAUBgNV +BAMMDXJvb3RhdXRob3JpdHkwHhcNMTUwNTE4MDkwMDQwWhcNMjMwODA0MDkwMDQw +WjAYMRYwFAYDVQQDDA1yb290YXV0aG9yaXR5MIIBIjANBgkqhkiG9w0BAQEFAAOC +AQ8AMIIBCgKCAQEAts1ijtBV92S2cOvpUMOSTp9c6A34nIGr0T5Nhz6XiqRVT+gv +dQgmkdKJQjbvR60y6jzltYFsI2MpGVXY8h/oAL81D/k7PDB2aREgyBfTPAhBHyGw +siR+2xYt5b/Zs99q5RdRqQNzNpLPJriIKvUsRyQWy1UiG2s7pRXQeA8qB0XtJdCj +kFIi+G2bDsaffspGeDOCqt7t+yqvRXfSES0c/l7DIHaiMbbp4//ZNML3RNgAjPz2 +hCezZ+wOYajOIyoSPK8IgICrhYFYxvgWxwbLDBEfC5B3jOQsySe10GoRAKZz1gBV +DmgReu81tYJmdgkc9zknnQtIFdA0ex+GvZlfWQIDAQABo1AwTjAdBgNVHQ4EFgQU +ZPNbPXdGW4mYYeOCnJMD26E3NF8wHwYDVR0jBBgwFoAUZPNbPXdGW4mYYeOCnJMD +26E3NF8wDAYDVR0TBAUwAwEB/zANBgkqhkiG9w0BAQsFAAOCAQEATzkZ97K777uZ +lQcduNX3ey4IbCiEzFA2zO5Blj+ilfIwNbZXNOgm/lqNvVGDYs6J1apJJe30vL3X +J+t2zsZWzzQzb9uIU37zYemt6m0fHrSrx/iy5lGNqt3HMfqEcOqSCOIK3PCTMz2/ +uyGe1iw33PVeWsm1JUybQ9IrU/huJjbgOHU4wab+8SJCM49ipArp68Fr6j4lcEaE +4rfRg1ZsvxiOyUB3qPn6wyL/JB8kOJ+QCBe498376eaem8AEFk0kQRh6hDaWtq/k +t6IIXQLjx+EBDVP/veK0UnVhKRP8YTOoV8ZiG1NcdlJmX/Uk7iAfevP7CkBfSN8W +r6AL284qtw== +-----END CERTIFICATE-----
diff --git a/http2/experimental/server_key.pem b/http2/experimental/server_key.pem new file mode 100644 index 0000000..1fd2324 --- /dev/null +++ b/http2/experimental/server_key.pem
@@ -0,0 +1,29 @@ +-----BEGIN ENCRYPTED PRIVATE KEY----- +MIIE5DAcBgoqhkiG9w0BDAEBMA4ECL7L6rj6uEHGAgIIAASCBMLbucyfqAkgCbhP +xNSHYllPMAv/dsIjtnsBwepCXPGkCBCuOAw/2FaCHjN9hBqL5V7fkrKeaemhm2YE +ycPtlHJYPDf3kEkyMjdZ9rIY6kePGfQizs2uJPcXj4YPyQ4HsfVXpOicKfQrouf5 +Mze9bGzeMN065q3iP4dYUMwHAyZYteXCsanQNHlqvsWli0W+H8St8fdsXefZhnv1 +qVatKWdNdWQ9t5MuljgNU2Vv56sHKEYXI0yLxk2QUMk8KlJfnmt8foYUsnPUXHmc +gIjLKwwVkpdololnEHSNu0cEOUPowjgJru+uMpn7vdNl7TPEQ9jbEgdNg4JwoYzU +0nao8WzjaSp7kzvZz0VFwKnk5AjstGvvuAWckADdq23QElbn/mF7AG1m/TBpYxzF +gTt37UdndS/AcvVznWVVrRP5iTSIawdIwvqI4s7rqsoE0GCcak+RhchgAz2gWKkS +oODUo0JL6pPVbJ3l4ebbaO6c99nDVc8dViPtc1EkStJEJ2O4kI4xgLSCr4Y9ahKn +oAaoSkX7Xxq3aQm+BzqSpLjdGL8atsqR/YVOIHYIl3gThvP0NfZGx1xHyvO5mCdZ +kHxSA7tKWxauZ3eQ2clbnzeRsl4El0WMHy/5K1ovene4v7sunmoXVtghBC8hK6eh +zMO9orex2PNQ/VQC7HCvtytunOVx1lkSBoNo7hR70igg6rW9H7UyoAoBOwMpT1xa +J6V62nqruTKOqFNfur7aHJGpHGtDb5/ickHeYCyPTvmGp67u4wChzKReeg02oECe +d1E5FKAcIa8s9TVOB6Z+HvTRNQZu2PsI6TJnjQRowvY9DAHiWTlJZBBY/pko3hxX +TsIeybpvRdEHpDWv86/iqtw1hv9CUxS/8ZTWUgBo+osShHW79FeDASr9FC4/Zn76 +ZDERTgV4YWlW/klVWcG2lFo7jix+OPXAB+ZQavLhlN1xdWBcIz1AUWjAM4hdPylW +HCX4PB9CQIPl2E7F+Y2p6nMcMWSJVBi5UIH7E9LfaBguXSzMmTk2Fw5p1aOQ6wfN +goVAMVwi8ppAVs741PfHdZ295xMmK/1LCxz5DeAdD/tsA/SYfT753GotioDuC7im +EyJ5JyvTr5I6RFFBuqt3NlUb3Hp16wP3B2x9DZiB6jxr0l341/NHgsyeBXkuIy9j +ON2mvpBPCJhS8kgWo3G0UyyKnx64tcgpGuSvZhGwPz843B6AbYyE6pMRfSWRMkMS +YZYa+VNKhR4ixdj07ocFZEWLVjCH7kxkE8JZXKt8jKYmkWd0lS1QVjgaKlO6lRa3 +q6SPJkhW6pvqobvcqVNXwi1XuzpZeEbuh0B7OTekFTTxx5g9XeDl56M8SVQ1KEhT +Q1t7H2Nba18WCB7cf+6PN0F0K0Jz1Kq7ZWaqEI/grX1m4RQuvNF5807sB/QKMO/Z +Gz3NXvHg5xTJRd/567lxPGkor0cE7qD1EZfmJ2HrBYXQ91bhgA7LToBuMZo6ZRXH +QfsanjbP4FPLMiGdQigLjj3A35L/f4sQOOVac/sRaFnm7pzcxsMvyVU/YtvGcjYE +xaOOVnamg661Wo0wksXoDjeSz/JIyyKO3Gwp1FSm2wGLjjy/Ehmqcqy8rvHuf07w +AUukhVtTNn4= +-----END ENCRYPTED PRIVATE KEY-----
diff --git a/http2/lib/http2.dart b/http2/lib/http2.dart new file mode 100644 index 0000000..3f1ed78 --- /dev/null +++ b/http2/lib/http2.dart
@@ -0,0 +1,48 @@ +// 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. + +/// This library provides an http/2 interface on top of a bidirectional stream +/// of bytes. +/// +/// The client and server sides can be created via [ClientTransportStream] and +/// [ServerTransportStream] respectively. Both sides can be configured via +/// settings (see [ClientSettings] and [ServerSettings]). The settings will be +/// communicated to the remote peer (if necessary) and will be valid during the +/// entire lifetime of the connection. +/// +/// A http/2 transport allows a client to open a bidirectional stream (see +/// [ClientTransportConnection.makeRequest]) and a server can open (or push) a +/// unidirectional stream to the client via [ServerTransportStream.push]. +/// +/// In both cases (unidirectional and bidirectional stream), one can send +/// headers and data to the other side (via [HeadersStreamMessage] and +/// [DataStreamMessage]). These messages are ordered and will arrive in the same +/// order as they were sent (data messages may be split up into multiple smaller +/// chunks or might be combined). +/// +/// In the most common case, each direction will send one [HeadersStreamMessage] +/// followed by zero or more [DataStreamMessage]s. +/// +/// Establishing a bidirectional stream of bytes to a server is up to the user +/// of this library. There are 3 common ways to achive this +/// +/// * connect to a server via SSL and use the ALPN (SSL) protocol extension +/// to negotiate with the server to speak http/2 (the ALPN protocol +/// identifier for http/2 is `h2`) +/// +/// * have prior knowledge about the server - i.e. know ahead of time that +/// the server will speak http/2 via an unencrypted tcp connection +/// +/// * use a http/1.1 connection and upgrade it to http/2 +/// +/// The first way is the most common way and can be done in Dart by using +/// `dart:io`s secure socket implementation (by using a `SecurityContext` and +/// including 'h2' in the list of protocols used for ALPN). +/// +/// A simple example on how to connect to a http/2 capable server and +/// requesting a resource is available at https://github.com/dart-lang/http2/blob/master/example/display_headers.dart. +library http2.http2; + +import 'transport.dart'; +export 'transport.dart';
diff --git a/http2/lib/multiprotocol_server.dart b/http2/lib/multiprotocol_server.dart new file mode 100644 index 0000000..320f96d --- /dev/null +++ b/http2/lib/multiprotocol_server.dart
@@ -0,0 +1,123 @@ +// Copyright (c) 2016 the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'dart:async'; +import 'dart:io'; + +import 'src/artificial_server_socket.dart'; +import 'transport.dart' as http2; + +/// Handles protocol negotiation with HTTP/1.1 and HTTP/2 clients. +/// +/// Given a (host, port) pair and a [SecurityContext], [MultiProtocolHttpServer] +/// will negotiate with the client whether HTTP/1.1 or HTTP/2 should be spoken. +/// +/// The user must supply 2 callback functions to [startServing], which: +/// * one handles HTTP/1.1 clients (called with a [HttpRequest]) +/// * one handles HTTP/2 clients (called with a [http2.ServerTransportStream]) +class MultiProtocolHttpServer { + final SecureServerSocket _serverSocket; + final http2.ServerSettings? _settings; + + late _ServerSocketController _http11Controller; + late HttpServer _http11Server; + + final StreamController<http2.ServerTransportStream> _http2Controller = + StreamController(); + Stream<http2.ServerTransportStream> get _http2Server => + _http2Controller.stream; + + final _http2Connections = <http2.ServerTransportConnection>{}; + + MultiProtocolHttpServer._(this._serverSocket, this._settings) { + _http11Controller = + _ServerSocketController(_serverSocket.address, _serverSocket.port); + _http11Server = HttpServer.listenOn(_http11Controller.stream); + } + + /// Binds a new [SecureServerSocket] with a security [context] at [port] and + /// [address] (see [SecureServerSocket.bind] for a description of supported + /// types for [address]). + /// + /// Optionally [settings] can be supplied which will be used for HTTP/2 + /// clients. + /// + /// See also [startServing]. + static Future<MultiProtocolHttpServer> bind( + address, int port, SecurityContext context, + {http2.ServerSettings? settings}) async { + context.setAlpnProtocols(['h2', 'h2-14', 'http/1.1'], true); + var secureServer = await SecureServerSocket.bind(address, port, context); + return MultiProtocolHttpServer._(secureServer, settings); + } + + /// The port this multi-protocol HTTP server runs on. + int get port => _serverSocket.port; + + /// The address this multi-protocol HTTP server runs on. + InternetAddress get address => _serverSocket.address; + + /// Starts listening for HTTP/1.1 and HTTP/2 clients and calls the given + /// callbacks for new clients. + /// + /// It is expected that [callbackHttp11] and [callbackHttp2] will never throw + /// an exception (i.e. these must take care of error handling themselves). + void startServing(void Function(HttpRequest) callbackHttp11, + void Function(http2.ServerTransportStream) callbackHttp2, + {void Function(dynamic error, StackTrace)? onError}) { + // 1. Start listening on the real [SecureServerSocket]. + _serverSocket.listen((SecureSocket socket) { + var protocol = socket.selectedProtocol; + if (protocol == null || protocol == 'http/1.1') { + _http11Controller.addHttp11Socket(socket); + } else if (protocol == 'h2' || protocol == 'h2-14') { + var connection = http2.ServerTransportConnection.viaSocket(socket, + settings: _settings); + _http2Connections.add(connection); + connection.incomingStreams.listen(_http2Controller.add, + onError: onError, + onDone: () => _http2Connections.remove(connection)); + } else { + socket.destroy(); + throw Exception('Unexpected negotiated ALPN protocol: $protocol.'); + } + }, onError: onError); + + // 2. Drain all incoming http/1.1 and http/2 connections and call the + // respective handlers. + _http11Server.listen(callbackHttp11); + _http2Server.listen(callbackHttp2); + } + + /// Closes this [MultiProtocolHttpServer]. + /// + /// Completes once everything has been successfully shut down. + Future close({bool force = false}) { + return _serverSocket.close().whenComplete(() { + var done1 = _http11Server.close(force: force); + Future done2 = Future.wait( + _http2Connections.map((c) => force ? c.terminate() : c.finish())); + return Future.wait([done1, done2]); + }); + } +} + +/// An internal helper class. +class _ServerSocketController { + final InternetAddress address; + final int port; + final StreamController<Socket> _controller = StreamController(); + + _ServerSocketController(this.address, this.port); + + ArtificialServerSocket get stream { + return ArtificialServerSocket(address, port, _controller.stream); + } + + void addHttp11Socket(Socket socket) { + _controller.add(socket); + } + + Future close() => _controller.close(); +}
diff --git a/http2/lib/src/artificial_server_socket.dart b/http2/lib/src/artificial_server_socket.dart new file mode 100644 index 0000000..1a486fe --- /dev/null +++ b/http2/lib/src/artificial_server_socket.dart
@@ -0,0 +1,35 @@ +// Copyright (c) 2016 the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'dart:async'; +import 'dart:io'; + +/// Custom implementation of the [ServerSocket] interface. +/// +/// This class can be used to create a [ServerSocket] using [Stream<Socket>] and +/// a [InternetAddress] and `port` (an example use case is to filter [Socket]s +/// and keep the [ServerSocket] interface for APIs that expect it, +/// e.g. `new HttpServer.listenOn()`). +class ArtificialServerSocket extends StreamView<Socket> + implements ServerSocket { + ArtificialServerSocket(this.address, this.port, Stream<Socket> stream) + : super(stream); + + // ######################################################################## + // These are the methods of [ServerSocket] in addition to [Stream<Socket>]. + // ######################################################################## + + @override + final InternetAddress address; + + @override + final int port; + + /// Closing of an [ArtificialServerSocket] is not possible and an exception + /// will be thrown when calling this method. + @override + Future<ServerSocket> close() async { + throw Exception('Did not expect close() to be called.'); + } +}
diff --git a/http2/lib/src/async_utils/async_utils.dart b/http2/lib/src/async_utils/async_utils.dart new file mode 100644 index 0000000..744fd90 --- /dev/null +++ b/http2/lib/src/async_utils/async_utils.dart
@@ -0,0 +1,134 @@ +// 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'; + +/// An interface for `StreamSink`-like classes to indicate whether adding data +/// would be buffered and when the buffer is empty again. +class BufferIndicator { + final StreamController _controller = StreamController.broadcast(sync: true); + + /// A state variable indicating whether buffereing would occur at the moment. + bool _wouldBuffer = true; + + /// Indicates whether calling [BufferedBytesWriter.add] would buffer the data + /// if called. + /// + /// This can be used at a higher level as a way to do custom buffering and + /// possibly prioritization. + bool get wouldBuffer { + return _wouldBuffer; + } + + /// Signals that no buffering is happening at the moment. + void markUnBuffered() { + if (_wouldBuffer) { + _wouldBuffer = false; + _controller.add(null); + } + } + + /// Signals that buffering starts to happen. + void markBuffered() { + _wouldBuffer = true; + } + + /// A broadcast stream notifying users that the [BufferedBytesWriter.add] + /// method would not buffer the data if called. + Stream get bufferEmptyEvents => _controller.stream; +} + +/// Contains a [StreamSink] and a [BufferIndicator] to indicate whether writes +/// to the sink would cause buffering. +/// +/// It uses the `pause signal` from the `sink.addStream()` as an indicator +/// whether the underlying stream cannot handle more data and would buffer. +class BufferedSink { + /// The indicator whether the underlying sink is buffering at the moment. + final bufferIndicator = BufferIndicator(); + + /// A intermediate [StreamController] used to catch pause signals and to + /// propagate the change via [bufferIndicator]. + final _controller = StreamController<List<int>>(sync: true); + + /// A future which completes once the sink has been closed. + late final Future _doneFuture; + + BufferedSink(StreamSink<List<int>> dataSink) { + bufferIndicator.markBuffered(); + + _controller + ..onListen = () { + bufferIndicator.markUnBuffered(); + } + ..onPause = () { + bufferIndicator.markBuffered(); + } + ..onResume = () { + bufferIndicator.markUnBuffered(); + } + ..onCancel = () { + // TODO: We may want to propagate cancel events as errors. + // Currently `_doneFuture` will just complete normally if the sink + // cancelled. + }; + _doneFuture = + Future.wait([_controller.stream.pipe(dataSink), dataSink.done]); + } + + /// The underlying sink. + StreamSink<List<int>> get sink => _controller; + + /// The future which will complete once this sink has been closed. + Future get doneFuture => _doneFuture; +} + +/// A small wrapper around [BufferedSink] which writes data in batches. +class BufferedBytesWriter { + /// A buffer which will be used for batching writes. + final BytesBuilder _builder = BytesBuilder(copy: false); + + /// The underlying [BufferedSink]. + final BufferedSink _bufferedSink; + + BufferedBytesWriter(StreamSink<List<int>> outgoing) + : _bufferedSink = BufferedSink(outgoing); + + /// An indicator whether the underlying sink is buffering at the moment. + BufferIndicator get bufferIndicator => _bufferedSink.bufferIndicator; + + /// Adds [data] immediately to the underlying buffer. + /// + /// If there is buffered data which was added with [addBufferedData] and it + /// has not been flushed with [flushBufferedData] an error will be thrown. + void add(List<int> data) { + if (_builder.length > 0) { + throw StateError( + 'Cannot trigger an asynchronous write while there is buffered data.'); + } + _bufferedSink.sink.add(data); + } + + /// Queues up [bytes] to be written. + void addBufferedData(List<int> bytes) { + _builder.add(bytes); + } + + /// Flushes all data which was enqueued by [addBufferedData]. + void flushBufferedData() { + if (_builder.length > 0) { + _bufferedSink.sink.add(_builder.takeBytes()); + } + } + + /// Closes this sink. + Future close() { + flushBufferedData(); + return _bufferedSink.sink.close().whenComplete(() => doneFuture); + } + + /// The future which will complete once this sink has been closed. + Future get doneFuture => _bufferedSink.doneFuture; +}
diff --git a/http2/lib/src/byte_utils.dart b/http2/lib/src/byte_utils.dart new file mode 100644 index 0000000..671e339 --- /dev/null +++ b/http2/lib/src/byte_utils.dart
@@ -0,0 +1,57 @@ +// 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:typed_data'; + +List<int> viewOrSublist(List<int> data, int offset, int length) { + if (data is Uint8List) { + return Uint8List.view(data.buffer, data.offsetInBytes + offset, length); + } else { + return data.sublist(offset, offset + length); + } +} + +int readInt64(List<int> bytes, int offset) { + var high = readInt32(bytes, offset); + var low = readInt32(bytes, offset + 4); + return high << 32 | low; +} + +int readInt32(List<int> bytes, int offset) { + return (bytes[offset] << 24) | + (bytes[offset + 1] << 16) | + (bytes[offset + 2] << 8) | + bytes[offset + 3]; +} + +int readInt24(List<int> bytes, int offset) { + return (bytes[offset] << 16) | (bytes[offset + 1] << 8) | bytes[offset + 2]; +} + +int readInt16(List<int> bytes, int offset) { + return (bytes[offset] << 8) | bytes[offset + 1]; +} + +void setInt64(List<int> bytes, int offset, int value) { + setInt32(bytes, offset, value >> 32); + setInt32(bytes, offset + 4, value & 0xffffffff); +} + +void setInt32(List<int> bytes, int offset, int value) { + bytes[offset] = (value >> 24) & 0xff; + bytes[offset + 1] = (value >> 16) & 0xff; + bytes[offset + 2] = (value >> 8) & 0xff; + bytes[offset + 3] = value & 0xff; +} + +void setInt24(List<int> bytes, int offset, int value) { + bytes[offset] = (value >> 16) & 0xff; + bytes[offset + 1] = (value >> 8) & 0xff; + bytes[offset + 2] = value & 0xff; +} + +void setInt16(List<int> bytes, int offset, int value) { + bytes[offset] = (value >> 8) & 0xff; + bytes[offset + 1] = value & 0xff; +}
diff --git a/http2/lib/src/connection.dart b/http2/lib/src/connection.dart new file mode 100644 index 0000000..32393d9 --- /dev/null +++ b/http2/lib/src/connection.dart
@@ -0,0 +1,491 @@ +// 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:convert' show utf8; + +import '../transport.dart'; +import 'connection_preface.dart'; +import 'flowcontrol/connection_queues.dart'; +import 'flowcontrol/queue_messages.dart'; +import 'flowcontrol/window.dart'; +import 'flowcontrol/window_handler.dart'; +import 'frames/frame_defragmenter.dart'; +import 'frames/frames.dart'; +import 'hpack/hpack.dart'; +import 'ping/ping_handler.dart'; +import 'settings/settings.dart'; +import 'streams/stream_handler.dart'; +import 'sync_errors.dart'; + +class ConnectionState { + /// The connection has been established, we're waiting for the settings frame + /// of the remote end. + static const int Initialized = 1; + + /// The connection has been established and is fully operational. + static const int Operational = 2; + + /// The connection is no longer accepting new streams or creating new streams. + static const int Finishing = 3; + + /// The connection has been terminated and cannot be used anymore. + static const int Terminated = 4; + + /// Whether we actively were finishing the connection. + static const int FinishingActive = 1; + + /// Whether we passively were finishing the connection. + static const int FinishingPassive = 2; + + int state = Initialized; + int finishingState = 0; + + ConnectionState(); + + bool get isInitialized => state == ConnectionState.Initialized; + + bool get isOperational => state == ConnectionState.Operational; + + bool get isFinishing => state == ConnectionState.Finishing; + + bool get isTerminated => state == ConnectionState.Terminated; + + bool get activeFinishing => + state == Finishing && (finishingState & FinishingActive) != 0; + + bool get passiveFinishing => + state == Finishing && (finishingState & FinishingPassive) != 0; + + @override + String toString() { + var message = ''; + + void add(bool condition, String flag) { + if (condition) { + if (message.isEmpty) { + message = flag; + } else { + message = '$message/$flag'; + } + } + } + + add(isInitialized, 'Initialized'); + add(isOperational, 'IsOperational'); + add(isFinishing, 'IsFinishing'); + add(isTerminated, 'IsTerminated'); + add(activeFinishing, 'ActiveFinishing'); + add(passiveFinishing, 'PassiveFinishing'); + + return message; + } +} + +abstract class Connection { + /// The settings the other end has acknowledged to use when communicating with + /// us. + final ActiveSettings acknowledgedSettings = ActiveSettings(); + + /// The settings we have to obey communicating with the other side. + final ActiveSettings peerSettings = ActiveSettings(); + + /// Whether this connection is a client connection. + final bool isClientConnection; + + /// Active state handler for this connection. + ActiveStateHandler? onActiveStateChanged; + + final Completer<void> _onInitialPeerSettingsReceived = Completer<void>(); + + /// Future which completes when the first SETTINGS frame is received from + /// the peer. + Future<void> get onInitialPeerSettingsReceived => + _onInitialPeerSettingsReceived.future; + + /// The HPack context for this connection. + final HPackContext _hpackContext = HPackContext(); + + /// The flow window for this connection of the peer. + final Window _peerWindow = Window(); + + /// The flow window for this connection of this end. + final Window _localWindow = Window(); + + /// Used for defragmenting PushPromise/Header frames. + final FrameDefragmenter _defragmenter = FrameDefragmenter(); + + /// The outgoing frames of this connection; + late FrameWriter _frameWriter; + + /// A subscription of incoming [Frame]s. + late StreamSubscription<Frame> _frameReaderSubscription; + + /// The incoming connection-level message queue. + late ConnectionMessageQueueIn _incomingQueue; + + /// The outgoing connection-level message queue. + late ConnectionMessageQueueOut _outgoingQueue; + + /// The ping handler used for making pings & handling remote pings. + late PingHandler _pingHandler; + + /// The settings handler used for changing settings & for handling remote + /// setting changes. + late SettingsHandler _settingsHandler; + + /// The set of active streams this connection has. + late StreamHandler _streams; + + /// The connection-level flow control window handler for outgoing messages. + late OutgoingConnectionWindowHandler _connectionWindowHandler; + + /// The state of this connection. + late ConnectionState _state; + + Connection(Stream<List<int>> incoming, StreamSink<List<int>> outgoing, + Settings settings, + {this.isClientConnection = true}) { + _setupConnection(incoming, outgoing, settings); + } + + /// Runs all setup necessary before new streams can be created with the remote + /// peer. + void _setupConnection(Stream<List<int>> incoming, + StreamSink<List<int>> outgoing, Settings settingsObject) { + // Setup frame reading. + var incomingFrames = + FrameReader(incoming, acknowledgedSettings).startDecoding(); + _frameReaderSubscription = incomingFrames.listen((Frame frame) { + _catchProtocolErrors(() => _handleFrameImpl(frame)); + }, onError: (error, stack) { + _terminate(ErrorCode.CONNECT_ERROR, causedByTransportError: true); + }, onDone: () { + // Ensure existing messages from lower levels are sent to the upper + // levels before we terminate everything. + _incomingQueue.forceDispatchIncomingMessages(); + _streams.forceDispatchIncomingMessages(); + + _terminate(ErrorCode.CONNECT_ERROR, causedByTransportError: true); + }); + + // Setup frame writing. + _frameWriter = FrameWriter(_hpackContext.encoder, outgoing, peerSettings); + _frameWriter.doneFuture.whenComplete(() { + _terminate(ErrorCode.CONNECT_ERROR, causedByTransportError: true); + }); + + // Setup handlers. + _settingsHandler = SettingsHandler(_hpackContext.encoder, _frameWriter, + acknowledgedSettings, peerSettings); + _pingHandler = PingHandler(_frameWriter); + + var settings = _decodeSettings(settingsObject); + + // Do the initial settings handshake (possibly with pushes disabled). + _settingsHandler.changeSettings(settings).catchError((error) { + // TODO: The [error] can contain sensitive information we now expose via + // a [Goaway] frame. We should somehow ensure we're only sending useful + // but non-sensitive information. + _terminate(ErrorCode.PROTOCOL_ERROR, + message: 'Failed to set initial settings (error: $error).'); + }); + + _settingsHandler.onInitialWindowSizeChange.listen((int difference) { + _catchProtocolErrors(() { + _streams.processInitialWindowSizeSettingChange(difference); + }); + }); + + // Setup the connection window handler, which keeps track of the + // size of the outgoing connection window. + _connectionWindowHandler = OutgoingConnectionWindowHandler(_peerWindow); + + var connectionWindowUpdater = + IncomingWindowHandler.connection(_frameWriter, _localWindow); + + // Setup queues for outgoing/incoming messages on the connection level. + _outgoingQueue = + ConnectionMessageQueueOut(_connectionWindowHandler, _frameWriter); + _incomingQueue = + ConnectionMessageQueueIn(connectionWindowUpdater, _catchProtocolErrors); + + if (isClientConnection) { + _streams = StreamHandler.client( + _frameWriter, + _incomingQueue, + _outgoingQueue, + _settingsHandler.peerSettings, + _settingsHandler.acknowledgedSettings, + _activeStateHandler); + } else { + _streams = StreamHandler.server( + _frameWriter, + _incomingQueue, + _outgoingQueue, + _settingsHandler.peerSettings, + _settingsHandler.acknowledgedSettings, + _activeStateHandler); + } + + // NOTE: We're not waiting until initial settings have been exchanged + // before we start using the connection (i.e. we don't wait for half a + // round-trip-time). + _state = ConnectionState(); + } + + List<Setting> _decodeSettings(Settings settings) { + var settingsList = <Setting>[]; + + // By default a endpoint can make an unlimited number of concurrent streams. + var concurrentStreamLimit = settings.concurrentStreamLimit; + if (concurrentStreamLimit != null) { + settingsList.add(Setting( + Setting.SETTINGS_MAX_CONCURRENT_STREAMS, concurrentStreamLimit)); + } + + // By default the stream level flow control window is 64 KiB. + var streamWindowSize = settings.streamWindowSize; + if (streamWindowSize != null) { + settingsList + .add(Setting(Setting.SETTINGS_INITIAL_WINDOW_SIZE, streamWindowSize)); + } + + if (settings is ClientSettings) { + // By default the server is allowed to do server pushes. + if (!settings.allowServerPushes) { + settingsList.add(Setting(Setting.SETTINGS_ENABLE_PUSH, 0)); + } + } else if (settings is ServerSettings) { + // No special server settings at the moment. + } else { + assert(false); + } + + return settingsList; + } + + /// Pings the remote peer (can e.g. be used for measuring latency). + Future ping() { + return _pingHandler.ping().catchError((e, s) { + return Future.error( + TransportException('The connection has been terminated.')); + }, test: (e) => e is TerminatedException); + } + + /// Finishes this connection. + Future finish() { + _finishing(active: true); + + // TODO: There is probably more we need to wait for. + return _streams.done.whenComplete(() => + Future.wait([_frameWriter.close(), _frameReaderSubscription.cancel()])); + } + + /// Terminates this connection forcefully. + Future terminate() { + return _terminate(ErrorCode.NO_ERROR); + } + + void _activeStateHandler(bool isActive) => + onActiveStateChanged?.call(isActive); + + /// Invokes the passed in closure and catches any exceptions. + void _catchProtocolErrors(void Function() fn) { + try { + fn(); + } on ProtocolException catch (error) { + _terminate(ErrorCode.PROTOCOL_ERROR, message: '$error'); + } on FlowControlException catch (error) { + _terminate(ErrorCode.FLOW_CONTROL_ERROR, message: '$error'); + } on FrameSizeException catch (error) { + _terminate(ErrorCode.FRAME_SIZE_ERROR, message: '$error'); + } on HPackDecodingException catch (error) { + _terminate(ErrorCode.PROTOCOL_ERROR, message: '$error'); + } on TerminatedException { + // We tried to perform an action even though the connection was already + // terminated. + // TODO: Can this even happen and if so, how should we propagate this + // error? + } catch (error) { + _terminate(ErrorCode.INTERNAL_ERROR, message: '$error'); + } + } + + void _handleFrameImpl(Frame? frame) { + // The first frame from the other side must be a [SettingsFrame], otherwise + // we terminate the connection. + if (_state.isInitialized) { + if (frame is! SettingsFrame) { + _terminate(ErrorCode.PROTOCOL_ERROR, + message: 'Expected to first receive a settings frame.'); + return; + } + _state.state = ConnectionState.Operational; + _onInitialPeerSettingsReceived.complete(); + } + + // Try to defragment [frame] if it is a Headers/PushPromise frame. + frame = _defragmenter.tryDefragmentFrame(frame); + if (frame == null) return; + + // Try to decode headers if it's a Headers/PushPromise frame. + // [This needs to be done even if the frames get ignored, since the entire + // connection shares one HPack compression context.] + if (frame is HeadersFrame) { + frame.decodedHeaders = + _hpackContext.decoder.decode(frame.headerBlockFragment); + } else if (frame is PushPromiseFrame) { + frame.decodedHeaders = + _hpackContext.decoder.decode(frame.headerBlockFragment); + } + + // Handle the frame as either a connection or a stream frame. + if (frame.header.streamId == 0) { + if (frame is SettingsFrame) { + _settingsHandler.handleSettingsFrame(frame); + } else if (frame is PingFrame) { + _pingHandler.processPingFrame(frame); + } else if (frame is WindowUpdateFrame) { + _connectionWindowHandler.processWindowUpdate(frame); + } else if (frame is GoawayFrame) { + _streams.processGoawayFrame(frame); + _finishing(active: false); + } else if (frame is UnknownFrame) { + // We can safely ignore these. + } else { + throw ProtocolException( + 'Cannot handle frame type ${frame.runtimeType} with stream-id 0.'); + } + } else { + _streams.processStreamFrame(_state, frame); + } + } + + void _finishing({bool active = true, String? message}) { + // If this connection is already dead, we return. + if (_state.isTerminated) return; + + // If this connection is already finishing, we make sure to store the + // passive bit, since this information is used by [StreamHandler]. + // + // Vice versa should not matter: If we started passively finishing, an + // active finish should be a NOP. + if (_state.isFinishing) { + if (!active) _state.finishingState |= ConnectionState.FinishingPassive; + return; + } + + assert(_state.isInitialized || _state.isOperational); + + // If we are actively finishing this connection, we'll send a + // GoawayFrame otherwise we'll just propagate the message. + if (active) { + _state.state = ConnectionState.Finishing; + _state.finishingState |= ConnectionState.FinishingActive; + + _outgoingQueue.enqueueMessage(GoawayMessage( + _streams.highestPeerInitiatedStream, + ErrorCode.NO_ERROR, + message != null ? utf8.encode(message) : [])); + } else { + _state.state = ConnectionState.Finishing; + _state.finishingState |= ConnectionState.FinishingPassive; + } + + _streams.startClosing(); + } + + /// Terminates this connection (if it is not already terminated). + /// + /// The returned future will never complete with an error. + Future _terminate(int errorCode, + {bool causedByTransportError = false, String? message}) { + // TODO: When do we complete here? + if (_state.state != ConnectionState.Terminated) { + _state.state = ConnectionState.Terminated; + + var cancelFuture = Future.sync(_frameReaderSubscription.cancel); + if (!causedByTransportError) { + _outgoingQueue.enqueueMessage(GoawayMessage( + _streams.highestPeerInitiatedStream, + errorCode, + message != null ? utf8.encode(message) : [])); + } + var closeFuture = _frameWriter.close().catchError((e, s) { + // We ignore any errors after writing to [GoawayFrame] + }); + + // Close all lower level handlers with an error message. + // (e.g. if there is a pending connection.ping(), it's returned + // Future will complete with this error). + var exception = TransportConnectionException( + errorCode, 'Connection is being forcefully terminated.'); + + // Close all streams & stream queues + _streams.terminate(exception); + + // Close the connection queues + _incomingQueue.terminate(exception); + _outgoingQueue.terminate(exception); + + _pingHandler.terminate(exception); + _settingsHandler.terminate(exception); + + return Future.wait([cancelFuture, closeFuture]).catchError((_) {}); + } + return Future.value(); + } +} + +class ClientConnection extends Connection implements ClientTransportConnection { + ClientConnection._(Stream<List<int>> incoming, StreamSink<List<int>> outgoing, + Settings settings) + : super(incoming, outgoing, settings, isClientConnection: true); + + factory ClientConnection(Stream<List<int>> incoming, + StreamSink<List<int>> outgoing, ClientSettings clientSettings) { + outgoing.add(CONNECTION_PREFACE); + return ClientConnection._(incoming, outgoing, clientSettings); + } + + @override + bool get isOpen => + !_state.isFinishing && !_state.isTerminated && _streams.canOpenStream; + + @override + ClientTransportStream makeRequest(List<Header> headers, + {bool endStream = false}) { + if (_state.isFinishing) { + throw StateError( + 'The http/2 connection is finishing and can therefore not be used to ' + 'make new streams.'); + } else if (_state.isTerminated) { + throw StateError( + 'The http/2 connection is no longer active and can therefore not be ' + 'used to make new streams.'); + } + var hStream = _streams.newStream(headers, endStream: endStream); + if (_streams.ranOutOfStreamIds) { + _finishing(active: true, message: 'Ran out of stream ids'); + } + return hStream; + } +} + +class ServerConnection extends Connection implements ServerTransportConnection { + ServerConnection._(Stream<List<int>> incoming, StreamSink<List<int>> outgoing, + Settings settings) + : super(incoming, outgoing, settings, isClientConnection: false); + + factory ServerConnection(Stream<List<int>> incoming, + StreamSink<List<int>> outgoing, ServerSettings serverSettings) { + var frameBytes = readConnectionPreface(incoming); + return ServerConnection._(frameBytes, outgoing, serverSettings); + } + + @override + Stream<ServerTransportStream> get incomingStreams => + _streams.incomingStreams.cast<ServerTransportStream>(); +}
diff --git a/http2/lib/src/connection_preface.dart b/http2/lib/src/connection_preface.dart new file mode 100644 index 0000000..c2bf652 --- /dev/null +++ b/http2/lib/src/connection_preface.dart
@@ -0,0 +1,115 @@ +// 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:math'; + +import 'byte_utils.dart'; + +/// This is a set of bytes with which a client connection begins in the normal +/// case. It can be used on a server to distinguish HTTP/1.1 and HTTP/2 clients. +const List<int> CONNECTION_PREFACE = [ + 0x50, + 0x52, + 0x49, + 0x20, + 0x2a, + 0x20, + 0x48, + 0x54, + 0x54, + 0x50, + 0x2f, + 0x32, + 0x2e, + 0x30, + 0x0d, + 0x0a, + 0x0d, + 0x0a, + 0x53, + 0x4d, + 0x0d, + 0x0a, + 0x0d, + 0x0a +]; + +/// Reads the connection preface from [incoming]. +/// +/// The returned `Stream` will be a duplicate of `incoming` without the +/// connection preface. If an error occurs while reading the connection +/// preface, the returned stream will have only an error. +Stream<List<int>> readConnectionPreface(Stream<List<int>> incoming) { + final result = StreamController<List<int>>(); + late StreamSubscription subscription; + var connectionPrefaceRead = false; + var prefaceBuffer = <int>[]; + var terminated = false; + + void terminate(Object error) { + if (!terminated) { + result.addError(error); + result.close(); + subscription.cancel(); + } + terminated = true; + } + + bool compareConnectionPreface(List<int> data) { + for (var i = 0; i < CONNECTION_PREFACE.length; i++) { + if (data[i] != CONNECTION_PREFACE[i]) { + terminate('Connection preface does not match.'); + return false; + } + } + connectionPrefaceRead = true; + return true; + } + + void onData(List<int> data) { + if (connectionPrefaceRead) { + // Forward data after reading preface. + result.add(data); + } else { + if (prefaceBuffer.isEmpty && data.length > CONNECTION_PREFACE.length) { + if (!compareConnectionPreface(data)) return; + data = data.sublist(CONNECTION_PREFACE.length); + } else if (prefaceBuffer.length < CONNECTION_PREFACE.length) { + var remaining = CONNECTION_PREFACE.length - prefaceBuffer.length; + + var end = min(data.length, remaining); + var part1 = viewOrSublist(data, 0, end); + var part2 = viewOrSublist(data, end, data.length - end); + prefaceBuffer.addAll(part1); + + if (prefaceBuffer.length == CONNECTION_PREFACE.length) { + if (!compareConnectionPreface(prefaceBuffer)) return; + } + data = part2; + } + if (data.isNotEmpty) { + result.add(data); + } + } + } + + result.onListen = () { + subscription = incoming.listen(onData, + onError: (Object e, StackTrace s) => result.addError(e, s), + onDone: () { + if (!connectionPrefaceRead) { + terminate('EOS before connection preface could be read.'); + } else { + result.close(); + } + }); + result + ..onPause = subscription.pause + ..onResume = subscription.resume + ..onCancel = subscription.cancel; + }; + + return result.stream; +}
diff --git a/http2/lib/src/error_handler.dart b/http2/lib/src/error_handler.dart new file mode 100644 index 0000000..d599dc2 --- /dev/null +++ b/http2/lib/src/error_handler.dart
@@ -0,0 +1,105 @@ +// 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 'sync_errors.dart'; + +/// Used by classes which may be terminated from the outside. +class TerminatableMixin { + bool _terminated = false; + + /// Terminates this stream message queue. Further operations on it will fail. + void terminate([error]) { + if (!wasTerminated) { + _terminated = true; + onTerminated(error); + } + } + + bool get wasTerminated => _terminated; + + void onTerminated(Object? error) { + // Subclasses can override this method if they want. + } + + T ensureNotTerminatedSync<T>(T Function() f) { + if (wasTerminated) { + throw TerminatedException(); + } + return f(); + } + + Future ensureNotTerminatedAsync(Future Function() f) { + if (wasTerminated) { + return Future.error(TerminatedException()); + } + return f(); + } +} + +/// Used by classes which may be cancelled. +class CancellableMixin { + bool _cancelled = false; + final _cancelCompleter = Completer<void>.sync(); + + Future<void> get onCancel => _cancelCompleter.future; + + /// Cancel this stream message queue. Further operations on it will fail. + void cancel() { + if (!wasCancelled) { + _cancelled = true; + _cancelCompleter.complete(); + } + } + + bool get wasCancelled => _cancelled; +} + +/// Used by classes which may be closed. +class ClosableMixin { + bool _closing = false; + final Completer _completer = Completer(); + + Future get done => _completer.future; + + bool get isClosing => _closing; + bool get wasClosed => _completer.isCompleted; + + void startClosing() { + if (!_closing) { + _closing = true; + + onClosing(); + } + onCheckForClose(); + } + + void onCheckForClose() { + // Subclasses can override this method if they want. + } + + void onClosing() { + // Subclasses can override this method if they want. + } + + dynamic ensureNotClosingSync(dynamic Function() f) { + if (isClosing) { + throw StateError('Was in the process of closing.'); + } + return f(); + } + + void closeWithValue([value]) { + if (!wasClosed) { + _completer.complete(value); + } + } + + void closeWithError(error) { + if (!wasClosed) { + _completer.complete(error); + } + } +}
diff --git a/http2/lib/src/flowcontrol/connection_queues.dart b/http2/lib/src/flowcontrol/connection_queues.dart new file mode 100644 index 0000000..b624787 --- /dev/null +++ b/http2/lib/src/flowcontrol/connection_queues.dart
@@ -0,0 +1,356 @@ +// 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. + +// TODO: Take priorities into account. +// TODO: Properly fragment large data frames, so they are not taking up too much +// bandwidth. + +import 'dart:async'; +import 'dart:collection'; + +import '../../transport.dart'; + +import '../byte_utils.dart'; +import '../error_handler.dart'; +import '../frames/frames.dart'; + +import 'queue_messages.dart'; +import 'stream_queues.dart'; +import 'window_handler.dart'; + +/// The last place before messages coming from the application get encoded and +/// send as [Frame]s. +/// +/// It will convert [Message]s from higher layers and send them via [Frame]s. +/// +/// - It will queue messages until the connection-level flow control window +/// allows sending the message and the underlying [StreamSink] is not +/// buffering. +/// - It will use a [FrameWriter] to write a new frame to the connection. +// TODO: Make [StreamsHandler] call [connectionOut.startClosing()] once +// * all streams have been closed +// * the connection state is finishing +class ConnectionMessageQueueOut extends Object + with TerminatableMixin, ClosableMixin { + /// The handler which will be used for increasing the connection-level flow + /// control window. + final OutgoingConnectionWindowHandler _connectionWindow; + + /// The buffered [Message]s which are to be delivered to the remote peer. + final Queue<Message> _messages = Queue<Message>(); + + /// The [FrameWriter] used for writing Headers/Data/PushPromise frames. + final FrameWriter _frameWriter; + + ConnectionMessageQueueOut(this._connectionWindow, this._frameWriter) { + _frameWriter.bufferIndicator.bufferEmptyEvents.listen((_) { + _trySendMessages(); + }); + _connectionWindow.positiveWindow.bufferEmptyEvents.listen((_) { + _trySendMessages(); + }); + } + + /// The number of pending messages which haven't been written to the wire. + int get pendingMessages => _messages.length; + + /// Enqueues a new [Message] which should be delivered to the remote peer. + void enqueueMessage(Message message) { + ensureNotClosingSync(() { + if (!wasTerminated) { + _messages.addLast(message); + _trySendMessages(); + } + }); + } + + @override + void onTerminated(error) { + _messages.clear(); + closeWithError(error); + } + + @override + void onCheckForClose() { + if (isClosing && _messages.isEmpty) { + closeWithValue(); + } + } + + void _trySendMessages() { + if (!wasTerminated) { + // We can make progress if + // * there is at least one message to send + // * the underlying frame writer / sink / socket doesn't block + // * either one + // * the next message is a non-flow control message (e.g. headers) + // * the connection window is positive + + if (_messages.isNotEmpty && + !_frameWriter.bufferIndicator.wouldBuffer && + (!_connectionWindow.positiveWindow.wouldBuffer || + _messages.first is! DataMessage)) { + _trySendMessage(); + + // If we have more messages and we can send them, we'll run them + // using `Timer.run()` to let other things get in-between. + if (_messages.isNotEmpty && + !_frameWriter.bufferIndicator.wouldBuffer && + (!_connectionWindow.positiveWindow.wouldBuffer || + _messages.first is! DataMessage)) { + // TODO: If all the frame writer methods would return the + // number of bytes written, we could just say, we loop here until 10kb + // and after words, we'll make `Timer.run()`. + Timer.run(_trySendMessages); + } else { + onCheckForClose(); + } + } + } + } + + void _trySendMessage() { + var message = _messages.first; + if (message is HeadersMessage) { + _messages.removeFirst(); + _frameWriter.writeHeadersFrame(message.streamId, message.headers, + endStream: message.endStream); + } else if (message is PushPromiseMessage) { + _messages.removeFirst(); + _frameWriter.writePushPromiseFrame( + message.streamId, message.promisedStreamId, message.headers); + } else if (message is DataMessage) { + _messages.removeFirst(); + + if (_connectionWindow.peerWindowSize >= message.bytes.length) { + _connectionWindow.decreaseWindow(message.bytes.length); + _frameWriter.writeDataFrame(message.streamId, message.bytes, + endStream: message.endStream); + } else { + // NOTE: We need to fragment the DataMessage. + // TODO: Do not fragment if the number of bytes we can send is too low + var len = _connectionWindow.peerWindowSize; + var head = viewOrSublist(message.bytes, 0, len); + var tail = + viewOrSublist(message.bytes, len, message.bytes.length - len); + + _connectionWindow.decreaseWindow(head.length); + _frameWriter.writeDataFrame(message.streamId, head, endStream: false); + + var tailMessage = + DataMessage(message.streamId, tail, message.endStream); + _messages.addFirst(tailMessage); + } + } else if (message is ResetStreamMessage) { + _messages.removeFirst(); + _frameWriter.writeRstStreamFrame(message.streamId, message.errorCode); + } else if (message is GoawayMessage) { + _messages.removeFirst(); + _frameWriter.writeGoawayFrame( + message.lastStreamId, message.errorCode, message.debugData); + } else { + throw StateError('Unexpected message in queue: ${message.runtimeType}'); + } + } +} + +/// The first place an incoming stream message gets delivered to. +/// +/// The [ConnectionMessageQueueIn] will be given [Frame]s which were sent to +/// any stream on this connection. +/// +/// - It will extract the necessary data from the [Frame] and store it in a new +/// [Message] object. +/// - It will multiplex the created [Message]es to a stream-specific +/// [StreamMessageQueueIn]. +/// - If the [StreamMessageQueueIn] cannot accept more data, the data will be +/// buffered until it can. +/// - [DataMessage]s which have been successfully delivered to a stream-specific +/// [StreamMessageQueueIn] will increase the flow control window for the +/// connection. +/// +/// Incoming [DataFrame]s will decrease the flow control window the peer has +/// available. +// TODO: Make [StreamsHandler] call [connectionOut.startClosing()] once +// * all streams have been closed +// * the connection state is finishing +class ConnectionMessageQueueIn extends Object + with TerminatableMixin, ClosableMixin { + /// The handler which will be used for increasing the connection-level flow + /// control window. + final IncomingWindowHandler _windowUpdateHandler; + + /// Catches any protocol errors and acts upon them. + final Function _catchProtocolErrors; + + /// A mapping from stream-id to the corresponding stream-specific + /// [StreamMessageQueueIn]. + final Map<int, StreamMessageQueueIn> _stream2messageQueue = {}; + + /// A buffer for [Message]s which cannot be received by their + /// [StreamMessageQueueIn]. + final Map<int, Queue<Message>> _stream2pendingMessages = {}; + + /// The number of pending messages which haven't been delivered + /// to the stream-specific queue. (for debugging purposes) + int _count = 0; + + ConnectionMessageQueueIn( + this._windowUpdateHandler, this._catchProtocolErrors); + + @override + void onTerminated(error) { + // NOTE: The higher level will be shutdown first, so all streams + // should have been removed at this point. + assert(_stream2messageQueue.isEmpty); + assert(_stream2pendingMessages.isEmpty); + closeWithError(error); + } + + @override + void onCheckForClose() { + if (isClosing) { + assert(_stream2messageQueue.isEmpty == _stream2pendingMessages.isEmpty); + if (_stream2messageQueue.isEmpty) { + closeWithValue(); + } + } + } + + /// The number of pending messages which haven't been delivered + /// to the stream-specific queue. (for debugging purposes) + int get pendingMessages => _count; + + /// Registers a stream specific [StreamMessageQueueIn] for a new stream id. + void insertNewStreamMessageQueue(int streamId, StreamMessageQueueIn mq) { + if (_stream2messageQueue.containsKey(streamId)) { + throw ArgumentError( + 'Cannot register a SteramMessageQueueIn for the same streamId ' + 'multiple times'); + } + + var pendingMessages = Queue<Message>(); + _stream2pendingMessages[streamId] = pendingMessages; + _stream2messageQueue[streamId] = mq; + + mq.bufferIndicator.bufferEmptyEvents.listen((_) { + _catchProtocolErrors(() { + _tryDispatch(streamId, mq, pendingMessages); + }); + }); + } + + /// Removes a stream id and its message queue from this connection-level + /// message queue. + void removeStreamMessageQueue(int streamId) { + _stream2pendingMessages.remove(streamId); + _stream2messageQueue.remove(streamId); + } + + /// Processes an incoming [DataFrame] which is addressed to a specific stream. + void processDataFrame(DataFrame frame) { + var streamId = frame.header.streamId; + var message = DataMessage(streamId, frame.bytes, frame.hasEndStreamFlag); + + _windowUpdateHandler.gotData(message.bytes.length); + _addMessage(streamId, message); + } + + /// If a [DataFrame] will be ignored, this method will take the minimal + /// action necessary. + void processIgnoredDataFrame(DataFrame frame) { + _windowUpdateHandler.gotData(frame.bytes.length); + } + + /// Processes an incoming [HeadersFrame] which is addressed to a specific + /// stream. + void processHeadersFrame(HeadersFrame frame) { + var streamId = frame.header.streamId; + var message = + HeadersMessage(streamId, frame.decodedHeaders, frame.hasEndStreamFlag); + // NOTE: Header frames do not affect flow control - only data frames do. + _addMessage(streamId, message); + } + + /// Processes an incoming [PushPromiseFrame] which is addressed to a specific + /// stream. + void processPushPromiseFrame( + PushPromiseFrame frame, ClientTransportStream pushedStream) { + var streamId = frame.header.streamId; + var message = PushPromiseMessage(streamId, frame.decodedHeaders, + frame.promisedStreamId, pushedStream, false); + + // NOTE: + // * Header frames do not affect flow control - only data frames do. + // * At this point we might reorder a push message earlier than + // data/headers messages. + _addPushMessage(streamId, message); + } + + void _addMessage(int streamId, Message message) { + _count++; + + // TODO: Do we need to do a runtime check here and + // raise a protocol error if we cannot find the registered stream? + var streamMQ = _stream2messageQueue[streamId]!; + var pendingMessages = _stream2pendingMessages[streamId]!; + pendingMessages.addLast(message); + _tryDispatch(streamId, streamMQ, pendingMessages); + } + + void _addPushMessage(int streamId, PushPromiseMessage message) { + _count++; + + // TODO: Do we need to do a runtime check here and + // raise a protocol error if we cannot find the registered stream? + var streamMQ = _stream2messageQueue[streamId]!; + streamMQ.enqueueMessage(message); + } + + void _tryDispatch( + int streamId, StreamMessageQueueIn mq, Queue<Message> pendingMessages) { + var bytesDeliveredToStream = 0; + while (!mq.bufferIndicator.wouldBuffer && pendingMessages.isNotEmpty) { + _count--; + + var message = pendingMessages.removeFirst(); + if (message is DataMessage) { + bytesDeliveredToStream += message.bytes.length; + } + mq.enqueueMessage(message); + if (message.endStream) { + assert(pendingMessages.isEmpty); + + _stream2messageQueue.remove(streamId); + _stream2pendingMessages.remove(streamId); + } + } + if (bytesDeliveredToStream > 0) { + _windowUpdateHandler.dataProcessed(bytesDeliveredToStream); + } + + onCheckForClose(); + } + + void forceDispatchIncomingMessages() { + final toBeRemoved = <int>{}; + _stream2pendingMessages.forEach((int streamId, Queue<Message> messages) { + final mq = _stream2messageQueue[streamId]!; + while (messages.isNotEmpty) { + _count--; + final message = messages.removeFirst(); + mq.enqueueMessage(message); + if (message.endStream) { + toBeRemoved.add(streamId); + break; + } + } + }); + + for (final streamId in toBeRemoved) { + _stream2messageQueue.remove(streamId); + _stream2pendingMessages.remove(streamId); + } + } +}
diff --git a/http2/lib/src/flowcontrol/queue_messages.dart b/http2/lib/src/flowcontrol/queue_messages.dart new file mode 100644 index 0000000..5c71228 --- /dev/null +++ b/http2/lib/src/flowcontrol/queue_messages.dart
@@ -0,0 +1,75 @@ +// 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 '../../transport.dart'; + +/// The subclasses of [Message] are objects that are coming from the +/// connection layer on top of frames. +/// +/// Messages on a HTTP/2 stream will be represented by a different class +/// hierarchy. +abstract class Message { + final int streamId; + final bool endStream; + + Message(this.streamId, this.endStream); +} + +class HeadersMessage extends Message { + final List<Header> headers; + + HeadersMessage(int streamId, this.headers, bool endStream) + : super(streamId, endStream); + + @override + String toString() => + 'HeadersMessage(headers: ${headers.length}, endStream: $endStream)'; +} + +class DataMessage extends Message { + final List<int> bytes; + + DataMessage(int streamId, this.bytes, bool endStream) + : super(streamId, endStream); + + @override + String toString() => + 'DataMessage(bytes: ${bytes.length}, endStream: $endStream)'; +} + +class PushPromiseMessage extends Message { + final List<Header> headers; + final int promisedStreamId; + final ClientTransportStream pushedStream; + + PushPromiseMessage(int streamId, this.headers, this.promisedStreamId, + this.pushedStream, bool endStream) + : super(streamId, endStream); + + @override + String toString() => 'PushPromiseMessage(bytes: ${headers.length}, ' + 'promisedStreamId: $promisedStreamId, endStream: $endStream)'; +} + +class ResetStreamMessage extends Message { + final int errorCode; + + ResetStreamMessage(int streamId, this.errorCode) : super(streamId, false); + + @override + String toString() => 'ResetStreamMessage(errorCode: $errorCode)'; +} + +class GoawayMessage extends Message { + final int lastStreamId; + final int errorCode; + final List<int> debugData; + + GoawayMessage(this.lastStreamId, this.errorCode, this.debugData) + : super(0, false); + + @override + String toString() => 'GoawayMessage(lastStreamId: $lastStreamId, ' + 'errorCode: $errorCode, debugData: ${debugData.length})'; +}
diff --git a/http2/lib/src/flowcontrol/stream_queues.dart b/http2/lib/src/flowcontrol/stream_queues.dart new file mode 100644 index 0000000..e2e82e5 --- /dev/null +++ b/http2/lib/src/flowcontrol/stream_queues.dart
@@ -0,0 +1,332 @@ +// 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:collection'; + +import '../../transport.dart'; +import '../async_utils/async_utils.dart'; +import '../byte_utils.dart'; +import '../error_handler.dart'; + +import 'connection_queues.dart'; +import 'queue_messages.dart'; +import 'window_handler.dart'; + +/// This class will buffer any headers/data messages in the order they were +/// added. +/// +/// It will ensure that we never send more data than the remote flow control +/// window allows. +class StreamMessageQueueOut extends Object + with TerminatableMixin, ClosableMixin { + /// The id of the stream this message queue belongs to. + final int streamId; + + /// The stream-level flow control handler. + final OutgoingStreamWindowHandler streamWindow; + + /// The underlying connection-level message queue. + final ConnectionMessageQueueOut connectionMessageQueue; + + /// A indicator for whether this queue is currently buffering. + final BufferIndicator bufferIndicator = BufferIndicator(); + + /// Buffered [Message]s which will be written to the underlying connection + /// if the flow control window allows so. + final Queue<Message> _messages = Queue<Message>(); + + /// Debugging data on how much data should be written to the underlying + /// connection message queue. + int toBeWrittenBytes = 0; + + /// Debugging data on how much data was written to the underlying connection + /// message queue. + int writtenBytes = 0; + + StreamMessageQueueOut( + this.streamId, this.streamWindow, this.connectionMessageQueue) { + streamWindow.positiveWindow.bufferEmptyEvents.listen((_) { + if (!wasTerminated) { + _trySendData(); + } + }); + if (streamWindow.positiveWindow.wouldBuffer) { + bufferIndicator.markBuffered(); + } else { + bufferIndicator.markUnBuffered(); + } + } + + /// Debugging data about how many messages are pending to be written to the + /// connection message queue. + int get pendingMessages => _messages.length; + + /// Enqueues a new [Message] which is to be delivered to the connection + /// message queue. + void enqueueMessage(Message message) { + if (message is! ResetStreamMessage) ensureNotClosingSync(() {}); + if (!wasTerminated) { + if (message.endStream) startClosing(); + + if (message is DataMessage) { + toBeWrittenBytes += message.bytes.length; + } + + _messages.addLast(message); + _trySendData(); + + if (_messages.isNotEmpty) { + bufferIndicator.markBuffered(); + } + } + } + + @override + void onTerminated(error) { + _messages.clear(); + closeWithError(error); + } + + @override + void onCheckForClose() { + if (isClosing && _messages.isEmpty) closeWithValue(); + } + + void _trySendData() { + var queueLenBefore = _messages.length; + + while (_messages.isNotEmpty) { + var message = _messages.first; + + if (message is HeadersMessage) { + _messages.removeFirst(); + connectionMessageQueue.enqueueMessage(message); + } else if (message is DataMessage) { + var bytesAvailable = streamWindow.peerWindowSize; + if (bytesAvailable > 0 || message.bytes.isEmpty) { + _messages.removeFirst(); + + // Do we need to fragment? + var messageToSend = message; + var messageBytes = message.bytes; + // TODO: Do not fragment if the number of bytes we can send is too low + if (messageBytes.length > bytesAvailable) { + var partA = viewOrSublist(messageBytes, 0, bytesAvailable); + var partB = viewOrSublist(messageBytes, bytesAvailable, + messageBytes.length - bytesAvailable); + var messageA = DataMessage(message.streamId, partA, false); + var messageB = + DataMessage(message.streamId, partB, message.endStream); + + // Put the second fragment back into the front of the queue. + _messages.addFirst(messageB); + + // Send the first fragment. + messageToSend = messageA; + } + + writtenBytes += messageToSend.bytes.length; + streamWindow.decreaseWindow(messageToSend.bytes.length); + connectionMessageQueue.enqueueMessage(messageToSend); + } else { + break; + } + } else if (message is ResetStreamMessage) { + _messages.removeFirst(); + connectionMessageQueue.enqueueMessage(message); + } else { + throw StateError('Unknown messages type: ${message.runtimeType}'); + } + } + if (queueLenBefore > 0 && _messages.isEmpty) { + bufferIndicator.markUnBuffered(); + } + + onCheckForClose(); + } +} + +/// Keeps a list of [Message] which should be delivered to the +/// [TransportStream]. +/// +/// It will keep messages up to the stream flow control window size if the +/// [messages] listener is paused. +class StreamMessageQueueIn extends Object + with TerminatableMixin, ClosableMixin, CancellableMixin { + /// The stream-level window our peer is using when sending us messages. + final IncomingWindowHandler windowHandler; + + /// A indicator whether this [StreamMessageQueueIn] is currently buffering. + final BufferIndicator bufferIndicator = BufferIndicator(); + + /// The pending [Message]s which are to be delivered via the [messages] + /// stream. + final Queue<Message> _pendingMessages = Queue<Message>(); + + /// The [StreamController] used for producing the [messages] stream. + final _incomingMessagesC = StreamController<StreamMessage>(); + + /// The [StreamController] used for producing the [serverPushes] stream. + final _serverPushStreamsC = StreamController<TransportStreamPush>(); + + StreamMessageQueueIn(this.windowHandler) { + // We start by marking it as buffered, since no one is listening yet and + // incoming messages will get buffered. + bufferIndicator.markBuffered(); + + _incomingMessagesC + ..onListen = () { + if (!wasClosed && !wasTerminated) { + _tryDispatch(); + _tryUpdateBufferIndicator(); + } + } + ..onPause = () { + _tryUpdateBufferIndicator(); + // TODO: Would we ever want to decrease the window size in this + // situation? + } + ..onResume = () { + if (!wasClosed && !wasTerminated) { + _tryDispatch(); + _tryUpdateBufferIndicator(); + } + } + ..onCancel = cancel; + + _serverPushStreamsC.onListen = () { + if (!wasClosed && !wasTerminated) { + _tryDispatch(); + _tryUpdateBufferIndicator(); + } + }; + } + + /// Debugging data: the number of pending messages in this queue. + int get pendingMessages => _pendingMessages.length; + + /// The stream of [StreamMessage]s which come from the remote peer. + Stream<StreamMessage> get messages => _incomingMessagesC.stream; + + /// The stream of [TransportStreamPush]es which come from the remote peer. + Stream<TransportStreamPush> get serverPushes => _serverPushStreamsC.stream; + + /// A lower layer enqueues a new [Message] which should be delivered to the + /// app. + void enqueueMessage(Message message) { + ensureNotClosingSync(() { + if (!wasTerminated) { + if (message is PushPromiseMessage) { + // NOTE: If server pushes were enabled, the client is responsible for + // either rejecting or handling them. + assert(!_serverPushStreamsC.isClosed); + var transportStreamPush = + TransportStreamPush(message.headers, message.pushedStream); + _serverPushStreamsC.add(transportStreamPush); + return; + } + + if (message is DataMessage) { + windowHandler.gotData(message.bytes.length); + } + _pendingMessages.add(message); + if (message.endStream) startClosing(); + + _tryDispatch(); + _tryUpdateBufferIndicator(); + } + }); + } + + @override + void onTerminated(exception) { + _pendingMessages.clear(); + if (!wasClosed) { + if (exception != null) { + _incomingMessagesC.addError(exception); + } + _incomingMessagesC.close(); + _serverPushStreamsC.close(); + closeWithError(exception); + } + } + + void onCloseCheck() { + if (isClosing && !wasClosed && _pendingMessages.isEmpty) { + _incomingMessagesC.close(); + _serverPushStreamsC.close(); + closeWithValue(); + } + } + + void forceDispatchIncomingMessages() { + while (_pendingMessages.isNotEmpty) { + final message = _pendingMessages.removeFirst(); + assert(!_incomingMessagesC.isClosed); + if (message is HeadersMessage) { + _incomingMessagesC.add(HeadersStreamMessage(message.headers, + endStream: message.endStream)); + } else if (message is DataMessage) { + if (message.bytes.isNotEmpty) { + _incomingMessagesC.add( + DataStreamMessage(message.bytes, endStream: message.endStream)); + } + } else { + // This can never happen. + assert(false); + } + if (message.endStream) { + onCloseCheck(); + } + } + } + + void _tryDispatch() { + while (!wasTerminated && _pendingMessages.isNotEmpty) { + var handled = wasCancelled; + + var message = _pendingMessages.first; + if (wasCancelled) { + _pendingMessages.removeFirst(); + } else if (message is HeadersMessage || message is DataMessage) { + assert(!_incomingMessagesC.isClosed); + if (_incomingMessagesC.hasListener && !_incomingMessagesC.isPaused) { + _pendingMessages.removeFirst(); + if (message is HeadersMessage) { + // NOTE: Header messages do not affect flow control - only + // data messages do. + _incomingMessagesC.add(HeadersStreamMessage(message.headers, + endStream: message.endStream)); + } else if (message is DataMessage) { + if (message.bytes.isNotEmpty) { + _incomingMessagesC.add(DataStreamMessage(message.bytes, + endStream: message.endStream)); + windowHandler.dataProcessed(message.bytes.length); + } + } else { + // This can never happen. + assert(false); + } + handled = true; + } + } + if (handled) { + if (message.endStream) { + onCloseCheck(); + } + } else { + break; + } + } + } + + void _tryUpdateBufferIndicator() { + if (_incomingMessagesC.isPaused || _pendingMessages.isNotEmpty) { + bufferIndicator.markBuffered(); + } else if (bufferIndicator.wouldBuffer && !_incomingMessagesC.isPaused) { + bufferIndicator.markUnBuffered(); + } + } +}
diff --git a/http2/lib/src/flowcontrol/window.dart b/http2/lib/src/flowcontrol/window.dart new file mode 100644 index 0000000..51b9016 --- /dev/null +++ b/http2/lib/src/flowcontrol/window.dart
@@ -0,0 +1,24 @@ +// 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. + +class Window { + static const int MAX_WINDOW_SIZE = (1 << 31) - 1; + + /// The size available in this window. + /// + /// The default flow control window for the entire connection and for new + /// streams is 65535). + /// + /// NOTE: This value can potentially become negative. + int _size; + + Window({int initialSize = (1 << 16) - 1}) : _size = initialSize; + + /// The current size of the flow control window. + int get size => _size; + + void modify(int difference) { + _size += difference; + } +}
diff --git a/http2/lib/src/flowcontrol/window_handler.dart b/http2/lib/src/flowcontrol/window_handler.dart new file mode 100644 index 0000000..6f9a5a6 --- /dev/null +++ b/http2/lib/src/flowcontrol/window_handler.dart
@@ -0,0 +1,162 @@ +// 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 '../async_utils/async_utils.dart'; +import '../frames/frames.dart'; +import '../sync_errors.dart'; + +import 'window.dart'; + +abstract class AbstractOutgoingWindowHandler { + /// The connection flow control window. + final Window _peerWindow; + + /// Indicates when the outgoing connection window turned positive and we can + /// send data frames again. + final BufferIndicator positiveWindow = BufferIndicator(); + + AbstractOutgoingWindowHandler(this._peerWindow) { + if (_peerWindow.size > 0) { + positiveWindow.markUnBuffered(); + } + } + + /// The flow control window size we use for sending data. We are not allowed + /// to let this window be negative. + int get peerWindowSize => _peerWindow.size; + + /// Process a window update frame received from the remote end. + void processWindowUpdate(WindowUpdateFrame frame) { + var increment = frame.windowSizeIncrement; + if ((_peerWindow.size + increment) > Window.MAX_WINDOW_SIZE) { + throw FlowControlException( + 'Window update received from remote peer would make flow control ' + 'window too large.'); + } else { + _peerWindow.modify(increment); + } + + // If we transitioned from an negative/empty window to a positive window + // we'll fire an event that more data frames can be sent now. + if (positiveWindow.wouldBuffer && _peerWindow.size > 0) { + positiveWindow.markUnBuffered(); + } + } + + /// Update the peer window by subtracting [numberOfBytes]. + /// + /// The remote peer will send us [WindowUpdateFrame]s which will increase + /// the window again at a later point in time. + void decreaseWindow(int numberOfBytes) { + _peerWindow.modify(-numberOfBytes); + if (_peerWindow.size <= 0) { + positiveWindow.markBuffered(); + } + } +} + +/// Handles the connection window for outgoing data frames. +class OutgoingConnectionWindowHandler extends AbstractOutgoingWindowHandler { + OutgoingConnectionWindowHandler(Window window) : super(window); +} + +/// Handles the window for outgoing messages to the peer. +class OutgoingStreamWindowHandler extends AbstractOutgoingWindowHandler { + OutgoingStreamWindowHandler(Window window) : super(window); + + /// Update the peer window by adding [difference] to it. + /// + /// + /// The remote peer has send a new [SettingsFrame] which updated the default + /// stream level [Setting.SETTINGS_INITIAL_WINDOW_SIZE]. This causes all + /// existing streams to update the flow stream-level flow control window. + void processInitialWindowSizeSettingChange(int difference) { + if ((_peerWindow.size + difference) > Window.MAX_WINDOW_SIZE) { + throw FlowControlException( + 'Window update received from remote peer would make flow control ' + 'window too large.'); + } else { + _peerWindow.modify(difference); + if (_peerWindow.size <= 0) { + positiveWindow.markBuffered(); + } else if (positiveWindow.wouldBuffer) { + positiveWindow.markUnBuffered(); + } + } + } +} + +/// Mirrors the flow control window the remote end is using. +class IncomingWindowHandler { + /// The [FrameWriter] used for writing [WindowUpdateFrame]s to the wire. + final FrameWriter _frameWriter; + + /// The mirror of the [Window] the remote end sees. + /// + /// If [_localWindow ] turns negative, it means the remote peer sent us more + /// data than we allowed it to send. + final Window _localWindow; + + /// The stream id this window handler is for (is `0` for connection level). + final int _streamId; + + IncomingWindowHandler.stream( + this._frameWriter, this._localWindow, this._streamId); + + IncomingWindowHandler.connection(this._frameWriter, this._localWindow) + : _streamId = 0; + + /// The current size for the incoming data window. + /// + /// (This should never get negative, otherwise the peer send us more data + /// than we told it to send.) + int get localWindowSize => _localWindow.size; + + /// Signals that we received [numberOfBytes] from the remote peer. + void gotData(int numberOfBytes) { + _localWindow.modify(-numberOfBytes); + + // If this turns negative, it means the remote end send us more data + // then we announced we can handle (i.e. the remote window size must be + // negative). + // + // NOTE: [_localWindow.size] tracks the amount of data we advertised that we + // can handle. The value can change in three situations: + // + // a) We received data from the remote end (we can handle now less data) + // => This is handled by [gotData]. + // + // b) We processed data from the remote end (we can handle now more data) + // => This is handled by [dataProcessed]. + // + // c) We increase/decrease the initial stream window size after the + // stream was created (newer streams will start with the changed + // initial stream window size). + // => This is not an issue, because we don't support changing the + // initial window size later on -- only during the initial + // settings exchange. Since streams (and therefore instances + // of [IncomingWindowHandler]) are only created after sending out + // our initial settings. + // + if (_localWindow.size < 0) { + throw FlowControlException( + 'Connection level flow control window became negative.'); + } + } + + /// Tell the peer we received [numberOfBytes] bytes. It will increase it's + /// sending window then. + /// + // TODO/FIXME: If we pause and don't want to get more data, we have to + // - either stop sending window update frames + // - or decreasing the window size + void dataProcessed(int numberOfBytes) { + _localWindow.modify(numberOfBytes); + + // TODO: This can be optimized by delaying the window update to + // send one update with a bigger difference than multiple small update + // frames. + _frameWriter.writeWindowUpdate(numberOfBytes, streamId: _streamId); + } +}
diff --git a/http2/lib/src/frames/frame_defragmenter.dart b/http2/lib/src/frames/frame_defragmenter.dart new file mode 100644 index 0000000..5ab2c72 --- /dev/null +++ b/http2/lib/src/frames/frame_defragmenter.dart
@@ -0,0 +1,91 @@ +// 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 '../sync_errors.dart'; + +import 'frames.dart'; + +/// Class used for defragmenting [HeadersFrame]s and [PushPromiseFrame]s. +// TODO: Somehow emit an error if too many continuation frames have been sent +// (since we're buffering all of them). +class FrameDefragmenter { + /// The current incomplete [HeadersFrame] fragment. + HeadersFrame? _headersFrame; + + /// The current incomplete [PushPromiseFrame] fragment. + PushPromiseFrame? _pushPromiseFrame; + + /// Tries to defragment [frame]. + /// + /// If the given [frame] is a [HeadersFrame] or a [PushPromiseFrame] which + /// needs de-fragmentation, it will be saved and `null` will be returned. + /// + /// If there is currently an incomplete [HeadersFrame] or [PushPromiseFrame] + /// saved, [frame] needs to be a [ContinuationFrame]. It will be added to the + /// saved frame. In case the defragmentation is complete, the defragmented + /// [HeadersFrame] or [PushPromiseFrame] will be returned. + /// + /// All other [Frame] types will be returned. + // TODO: Consider handling continuation frames without preceding + // headers/push-promise frame here instead of the call site? + Frame? tryDefragmentFrame(Frame? frame) { + if (_headersFrame != null) { + if (frame is ContinuationFrame) { + if (_headersFrame!.header.streamId != frame.header.streamId) { + throw ProtocolException( + 'Defragmentation: frames have different stream ids.'); + } + _headersFrame = _headersFrame!.addBlockContinuation(frame); + + if (frame.hasEndHeadersFlag) { + var frame = _headersFrame; + _headersFrame = null; + return frame; + } else { + return null; + } + } else { + throw ProtocolException( + 'Defragmentation: Incomplete frame must be followed by ' + 'continuation frame.'); + } + } else if (_pushPromiseFrame != null) { + if (frame is ContinuationFrame) { + if (_pushPromiseFrame!.header.streamId != frame.header.streamId) { + throw ProtocolException( + 'Defragmentation: frames have different stream ids.'); + } + _pushPromiseFrame = _pushPromiseFrame!.addBlockContinuation(frame); + + if (frame.hasEndHeadersFlag) { + var frame = _pushPromiseFrame; + _pushPromiseFrame = null; + return frame; + } else { + return null; + } + } else { + throw ProtocolException( + 'Defragmentation: Incomplete frame must be followed by ' + 'continuation frame.'); + } + } else { + if (frame is HeadersFrame) { + if (!frame.hasEndHeadersFlag) { + _headersFrame = frame; + return null; + } + } else if (frame is PushPromiseFrame) { + if (!frame.hasEndHeadersFlag) { + _pushPromiseFrame = frame; + return null; + } + } + } + + // If this frame is not relevant for header defragmentation, we pass it to + // the next stage. + return frame; + } +}
diff --git a/http2/lib/src/frames/frame_reader.dart b/http2/lib/src/frames/frame_reader.dart new file mode 100644 index 0000000..2d93f01 --- /dev/null +++ b/http2/lib/src/frames/frame_reader.dart
@@ -0,0 +1,284 @@ +// 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. + +part of http2.src.frames; + +/// Used for converting a `Stream<List<int>>` to a `Stream<Frame>`. +class FrameReader { + final Stream<List<int>> _inputStream; + + /// Connection settings which this reader needs to ensure the remote end is + /// complying with. + final ActiveSettings _localSettings; + + final _framesController = StreamController<Frame>(); + + FrameReader(this._inputStream, this._localSettings); + + /// Starts to listen on the input stream and decodes HTTP/2 transport frames. + Stream<Frame> startDecoding() { + var bufferedData = <List<int>>[]; + var bufferedLength = 0; + + FrameHeader? tryReadHeader() { + if (bufferedLength >= FRAME_HEADER_SIZE) { + // Get at least FRAME_HEADER_SIZE bytes in the first byte array. + _mergeLists(bufferedData, FRAME_HEADER_SIZE); + + // Read the frame header from the first byte array. + return _readFrameHeader(bufferedData[0], 0); + } + return null; + } + + Frame? tryReadFrame(FrameHeader header) { + var totalFrameLen = FRAME_HEADER_SIZE + header.length; + if (bufferedLength >= totalFrameLen) { + // Get the whole frame in the first byte array. + _mergeLists(bufferedData, totalFrameLen); + + // Read the frame. + var frame = _readFrame(header, bufferedData[0], FRAME_HEADER_SIZE); + + // Update bufferedData/bufferedLength + var firstChunkLen = bufferedData[0].length; + if (firstChunkLen == totalFrameLen) { + bufferedData.removeAt(0); + } else { + bufferedData[0] = viewOrSublist( + bufferedData[0], totalFrameLen, firstChunkLen - totalFrameLen); + } + bufferedLength -= totalFrameLen; + + return frame; + } + return null; + } + + _framesController.onListen = () { + FrameHeader? header; + + late StreamSubscription<List<int>> subscription; + + void terminateWithError(Object error, [StackTrace? stack]) { + header = null; + _framesController.addError(error, stack); + subscription.cancel(); + _framesController.close(); + } + + subscription = _inputStream.listen((List<int> data) { + bufferedData.add(data); + bufferedLength += data.length; + + try { + while (true) { + header ??= tryReadHeader(); + if (header != null) { + if (header!.length > _localSettings.maxFrameSize) { + terminateWithError( + FrameSizeException('Incoming frame is too big.')); + return; + } + + var frame = tryReadFrame(header!); + + if (frame != null) { + _framesController.add(frame); + header = null; + } else { + break; + } + } else { + break; + } + } + } catch (error, stack) { + terminateWithError(error, stack); + } + }, onError: (Object error, StackTrace stack) { + terminateWithError(error, stack); + }, onDone: () { + if (bufferedLength == 0) { + _framesController.close(); + } else { + terminateWithError(FrameSizeException( + 'Incoming byte stream ended with incomplete frame')); + } + }); + + _framesController + ..onPause = subscription.pause + ..onResume = subscription.resume; + }; + + return _framesController.stream; + } + + /// Combine combines/merges `List<int>`s of `bufferedData` until + /// `numberOfBytes` have been accumulated. + /// + /// After calling `mergeLists`, `bufferedData[0]` will contain at least + /// `numberOfBytes` bytes. + void _mergeLists(List<List<int>> bufferedData, int numberOfBytes) { + if (bufferedData[0].length < numberOfBytes) { + var numLists = 0; + var accumulatedLength = 0; + while (accumulatedLength < numberOfBytes && + numLists <= bufferedData.length) { + accumulatedLength += bufferedData[numLists++].length; + } + assert(accumulatedLength >= numberOfBytes); + var newList = Uint8List(accumulatedLength); + var offset = 0; + for (var i = 0; i < numLists; i++) { + var data = bufferedData[i]; + newList.setRange(offset, offset + data.length, data); + offset += data.length; + } + bufferedData[0] = newList; + bufferedData.removeRange(1, numLists); + } + } + + /// Reads a FrameHeader] from [bytes], starting at [offset]. + FrameHeader _readFrameHeader(List<int> bytes, int offset) { + var length = readInt24(bytes, offset); + var type = bytes[offset + 3]; + var flags = bytes[offset + 4]; + var streamId = readInt32(bytes, offset + 5) & 0x7fffffff; + + return FrameHeader(length, type, flags, streamId); + } + + /// Reads a [Frame] from [bytes], starting at [frameOffset]. + Frame _readFrame(FrameHeader header, List<int> bytes, int frameOffset) { + var frameEnd = frameOffset + header.length; + + var offset = frameOffset; + switch (header.type) { + case FrameType.DATA: + var padLength = 0; + if (_isFlagSet(header.flags, DataFrame.FLAG_PADDED)) { + _checkFrameLengthCondition((frameEnd - offset) >= 1); + padLength = bytes[offset++]; + } + var dataLen = frameEnd - offset - padLength; + _checkFrameLengthCondition(dataLen >= 0); + var dataBytes = viewOrSublist(bytes, offset, dataLen); + return DataFrame(header, padLength, dataBytes); + + case FrameType.HEADERS: + var padLength = 0; + if (_isFlagSet(header.flags, HeadersFrame.FLAG_PADDED)) { + _checkFrameLengthCondition((frameEnd - offset) >= 1); + padLength = bytes[offset++]; + } + int? streamDependency; + var exclusiveDependency = false; + int? weight; + if (_isFlagSet(header.flags, HeadersFrame.FLAG_PRIORITY)) { + _checkFrameLengthCondition((frameEnd - offset) >= 5); + exclusiveDependency = (bytes[offset] & 0x80) == 0x80; + streamDependency = readInt32(bytes, offset) & 0x7fffffff; + offset += 4; + weight = bytes[offset++]; + } + var headerBlockLen = frameEnd - offset - padLength; + _checkFrameLengthCondition(headerBlockLen >= 0); + var headerBlockFragment = viewOrSublist(bytes, offset, headerBlockLen); + return HeadersFrame(header, padLength, exclusiveDependency, + streamDependency, weight, headerBlockFragment); + + case FrameType.PRIORITY: + _checkFrameLengthCondition( + (frameEnd - offset) == PriorityFrame.FIXED_FRAME_LENGTH, + message: 'Priority frame length must be exactly 5 bytes.'); + var exclusiveDependency = (bytes[offset] & 0x80) == 0x80; + var streamDependency = readInt32(bytes, offset) & 0x7fffffff; + var weight = bytes[offset + 4]; + return PriorityFrame( + header, exclusiveDependency, streamDependency, weight); + + case FrameType.RST_STREAM: + _checkFrameLengthCondition( + (frameEnd - offset) == RstStreamFrame.FIXED_FRAME_LENGTH, + message: 'Rst frames must have a length of 4.'); + var errorCode = readInt32(bytes, offset); + return RstStreamFrame(header, errorCode); + + case FrameType.SETTINGS: + _checkFrameLengthCondition((header.length % 6) == 0, + message: 'Settings frame length must be a multiple of 6 bytes.'); + + var count = header.length ~/ 6; + var settings = <Setting>[]; + for (var i = 0; i < count; i++) { + var identifier = readInt16(bytes, offset + 6 * i); + var value = readInt32(bytes, offset + 6 * i + 2); + settings.add(Setting(identifier, value)); + } + var frame = SettingsFrame(header, settings); + if (frame.hasAckFlag) { + _checkFrameLengthCondition(header.length == 0, + message: 'Settings frame length must 0 for ACKs.'); + } + return frame; + + case FrameType.PUSH_PROMISE: + var padLength = 0; + if (_isFlagSet(header.flags, PushPromiseFrame.FLAG_PADDED)) { + _checkFrameLengthCondition((frameEnd - offset) >= 1); + padLength = bytes[offset++]; + } + var promisedStreamId = readInt32(bytes, offset) & 0x7fffffff; + offset += 4; + var headerBlockLen = frameEnd - offset - padLength; + _checkFrameLengthCondition(headerBlockLen >= 0); + var headerBlockFragment = viewOrSublist(bytes, offset, headerBlockLen); + return PushPromiseFrame( + header, padLength, promisedStreamId, headerBlockFragment); + + case FrameType.PING: + _checkFrameLengthCondition( + (frameEnd - offset) == PingFrame.FIXED_FRAME_LENGTH, + message: 'Ping frames must have a length of 8.'); + var opaqueData = readInt64(bytes, offset); + return PingFrame(header, opaqueData); + + case FrameType.GOAWAY: + _checkFrameLengthCondition((frameEnd - offset) >= 8); + var lastStreamId = readInt32(bytes, offset); + var errorCode = readInt32(bytes, offset + 4); + var debugData = viewOrSublist(bytes, offset + 8, header.length - 8); + return GoawayFrame(header, lastStreamId, errorCode, debugData); + + case FrameType.WINDOW_UPDATE: + _checkFrameLengthCondition( + (frameEnd - offset) == WindowUpdateFrame.FIXED_FRAME_LENGTH, + message: 'Window update frames must have a length of 4.'); + var windowSizeIncrement = readInt32(bytes, offset) & 0x7fffffff; + return WindowUpdateFrame(header, windowSizeIncrement); + + case FrameType.CONTINUATION: + var headerBlockFragment = + viewOrSublist(bytes, offset, frameEnd - offset); + return ContinuationFrame(header, headerBlockFragment); + + default: + // Unknown frames should be ignored according to spec. + return UnknownFrame( + header, viewOrSublist(bytes, offset, frameEnd - offset)); + } + } + + /// Checks that [condition] is `true` and raises an [FrameSizeException] + /// otherwise. + void _checkFrameLengthCondition(bool condition, + {String message = 'Frame not long enough.'}) { + if (!condition) { + throw FrameSizeException(message); + } + } +}
diff --git a/http2/lib/src/frames/frame_types.dart b/http2/lib/src/frames/frame_types.dart new file mode 100644 index 0000000..abab588 --- /dev/null +++ b/http2/lib/src/frames/frame_types.dart
@@ -0,0 +1,341 @@ +// 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. + +part of http2.src.frames; + +const int FRAME_HEADER_SIZE = 9; + +class FrameType { + static const int DATA = 0; + static const int HEADERS = 1; + static const int PRIORITY = 2; + static const int RST_STREAM = 3; + static const int SETTINGS = 4; + static const int PUSH_PROMISE = 5; + static const int PING = 6; + static const int GOAWAY = 7; + static const int WINDOW_UPDATE = 8; + static const int CONTINUATION = 9; +} + +class ErrorCode { + static const int NO_ERROR = 0; + static const int PROTOCOL_ERROR = 1; + static const int INTERNAL_ERROR = 2; + static const int FLOW_CONTROL_ERROR = 3; + static const int SETTINGS_TIMEOUT = 4; + static const int STREAM_CLOSED = 5; + static const int FRAME_SIZE_ERROR = 6; + static const int REFUSED_STREAM = 7; + static const int CANCEL = 8; + static const int COMPRESSION_ERROR = 9; + static const int CONNECT_ERROR = 10; + static const int ENHANCE_YOUR_CALM = 11; + static const int INADEQUATE_SECURITY = 12; + static const int HTTP_1_1_REQUIRED = 13; +} + +class FrameHeader { + final int length; + final int type; + final int flags; + final int streamId; + + FrameHeader(this.length, this.type, this.flags, this.streamId); + + Map toJson() => + {'length': length, 'type': type, 'flags': flags, 'streamId': streamId}; +} + +class Frame { + static const int MAX_LEN = (1 << 24) - 1; + + final FrameHeader header; + + Frame(this.header); + + Map toJson() => {'header': header.toJson()}; +} + +class DataFrame extends Frame { + static const int FLAG_END_STREAM = 0x1; + static const int FLAG_PADDED = 0x8; + + /// The number of padding bytes. + final int padLength; + + final List<int> bytes; + + DataFrame(FrameHeader header, this.padLength, this.bytes) : super(header); + + bool get hasEndStreamFlag => _isFlagSet(header.flags, FLAG_END_STREAM); + bool get hasPaddedFlag => _isFlagSet(header.flags, FLAG_PADDED); + + @override + Map toJson() => super.toJson() + ..addAll({ + 'padLength': padLength, + 'bytes (length)': bytes.length, + 'bytes (up to 4 bytes)': bytes.length > 4 ? bytes.sublist(0, 4) : bytes, + }); +} + +class HeadersFrame extends Frame { + static const int FLAG_END_STREAM = 0x1; + static const int FLAG_END_HEADERS = 0x4; + static const int FLAG_PADDED = 0x8; + static const int FLAG_PRIORITY = 0x20; + + // NOTE: This is the size a [HeadersFrame] can have in addition to padding + // and header block fragment data. + static const int MAX_CONSTANT_PAYLOAD = 6; + + /// The number of padding bytes (might be null). + final int padLength; + + final bool exclusiveDependency; + final int? streamDependency; + final int? weight; + final List<int> headerBlockFragment; + + HeadersFrame(FrameHeader header, this.padLength, this.exclusiveDependency, + this.streamDependency, this.weight, this.headerBlockFragment) + : super(header); + + /// This will be set from the outside after decoding. + late List<Header> decodedHeaders; + + bool get hasEndStreamFlag => _isFlagSet(header.flags, FLAG_END_STREAM); + bool get hasEndHeadersFlag => _isFlagSet(header.flags, FLAG_END_HEADERS); + bool get hasPaddedFlag => _isFlagSet(header.flags, FLAG_PADDED); + bool get hasPriorityFlag => _isFlagSet(header.flags, FLAG_PRIORITY); + + HeadersFrame addBlockContinuation(ContinuationFrame frame) { + var fragment = frame.headerBlockFragment; + var flags = header.flags | frame.header.flags; + var fh = FrameHeader( + header.length + fragment.length, header.type, flags, header.streamId); + + var mergedHeaderBlockFragment = + Uint8List(headerBlockFragment.length + fragment.length); + + mergedHeaderBlockFragment.setRange( + 0, headerBlockFragment.length, headerBlockFragment); + + mergedHeaderBlockFragment.setRange( + headerBlockFragment.length, mergedHeaderBlockFragment.length, fragment); + + return HeadersFrame(fh, padLength, exclusiveDependency, streamDependency, + weight, mergedHeaderBlockFragment); + } + + @override + Map toJson() => super.toJson() + ..addAll({ + 'padLength': padLength, + 'exclusiveDependency': exclusiveDependency, + 'streamDependency': streamDependency, + 'weight': weight, + 'headerBlockFragment (length)': headerBlockFragment.length + }); +} + +class PriorityFrame extends Frame { + static const int FIXED_FRAME_LENGTH = 5; + + final bool exclusiveDependency; + final int streamDependency; + final int weight; + + PriorityFrame(FrameHeader header, this.exclusiveDependency, + this.streamDependency, this.weight) + : super(header); + + @override + Map toJson() => super.toJson() + ..addAll({ + 'exclusiveDependency': exclusiveDependency, + 'streamDependency': streamDependency, + 'weight': weight, + }); +} + +class RstStreamFrame extends Frame { + static const int FIXED_FRAME_LENGTH = 4; + + final int errorCode; + + RstStreamFrame(FrameHeader header, this.errorCode) : super(header); + + @override + Map toJson() => super.toJson() + ..addAll({ + 'errorCode': errorCode, + }); +} + +class Setting { + static const int SETTINGS_HEADER_TABLE_SIZE = 1; + static const int SETTINGS_ENABLE_PUSH = 2; + static const int SETTINGS_MAX_CONCURRENT_STREAMS = 3; + static const int SETTINGS_INITIAL_WINDOW_SIZE = 4; + static const int SETTINGS_MAX_FRAME_SIZE = 5; + static const int SETTINGS_MAX_HEADER_LIST_SIZE = 6; + + final int identifier; + final int value; + + Setting(this.identifier, this.value); + + Map toJson() => {'identifier': identifier, 'value': value}; +} + +class SettingsFrame extends Frame { + static const int FLAG_ACK = 0x1; + + // A setting consist of a 2 byte identifier and a 4 byte value. + static const int SETTING_SIZE = 6; + + final List<Setting> settings; + + SettingsFrame(FrameHeader header, this.settings) : super(header); + + bool get hasAckFlag => _isFlagSet(header.flags, FLAG_ACK); + + @override + Map toJson() => super.toJson() + ..addAll({ + 'settings': settings.map((s) => s.toJson()).toList(), + }); +} + +class PushPromiseFrame extends Frame { + static const int FLAG_END_HEADERS = 0x4; + static const int FLAG_PADDED = 0x8; + + // NOTE: This is the size a [PushPromiseFrame] can have in addition to padding + // and header block fragment data. + static const int MAX_CONSTANT_PAYLOAD = 5; + + final int padLength; + final int promisedStreamId; + final List<int> headerBlockFragment; + + /// This will be set from the outside after decoding. + late List<Header> decodedHeaders; + + PushPromiseFrame(FrameHeader header, this.padLength, this.promisedStreamId, + this.headerBlockFragment) + : super(header); + + bool get hasEndHeadersFlag => _isFlagSet(header.flags, FLAG_END_HEADERS); + bool get hasPaddedFlag => _isFlagSet(header.flags, FLAG_PADDED); + + PushPromiseFrame addBlockContinuation(ContinuationFrame frame) { + var fragment = frame.headerBlockFragment; + var flags = header.flags | frame.header.flags; + var fh = FrameHeader( + header.length + fragment.length, header.type, flags, header.streamId); + + var mergedHeaderBlockFragment = + Uint8List(headerBlockFragment.length + fragment.length); + + mergedHeaderBlockFragment.setRange( + 0, headerBlockFragment.length, headerBlockFragment); + + mergedHeaderBlockFragment.setRange( + headerBlockFragment.length, mergedHeaderBlockFragment.length, fragment); + + return PushPromiseFrame( + fh, padLength, promisedStreamId, mergedHeaderBlockFragment); + } + + @override + Map toJson() => super.toJson() + ..addAll({ + 'padLength': padLength, + 'promisedStreamId': promisedStreamId, + 'headerBlockFragment (len)': headerBlockFragment.length, + }); +} + +class PingFrame extends Frame { + static const int FIXED_FRAME_LENGTH = 8; + + static const int FLAG_ACK = 0x1; + + final int opaqueData; + + PingFrame(FrameHeader header, this.opaqueData) : super(header); + + bool get hasAckFlag => _isFlagSet(header.flags, FLAG_ACK); + + @override + Map toJson() => super.toJson() + ..addAll({ + 'opaqueData': opaqueData, + }); +} + +class GoawayFrame extends Frame { + final int lastStreamId; + final int errorCode; + final List<int> debugData; + + GoawayFrame( + FrameHeader header, this.lastStreamId, this.errorCode, this.debugData) + : super(header); + + @override + Map toJson() => super.toJson() + ..addAll({ + 'lastStreamId': lastStreamId, + 'errorCode': errorCode, + 'debugData (length)': debugData.length, + }); +} + +class WindowUpdateFrame extends Frame { + static const int FIXED_FRAME_LENGTH = 4; + + final int windowSizeIncrement; + + WindowUpdateFrame(FrameHeader header, this.windowSizeIncrement) + : super(header); + + @override + Map toJson() => super.toJson() + ..addAll({ + 'windowSizeIncrement': windowSizeIncrement, + }); +} + +class ContinuationFrame extends Frame { + static const int FLAG_END_HEADERS = 0x4; + + final List<int> headerBlockFragment; + + ContinuationFrame(FrameHeader header, this.headerBlockFragment) + : super(header); + + bool get hasEndHeadersFlag => _isFlagSet(header.flags, FLAG_END_HEADERS); + + @override + Map toJson() => super.toJson() + ..addAll({ + 'headerBlockFragment (length)': headerBlockFragment.length, + }); +} + +class UnknownFrame extends Frame { + final List<int> data; + + UnknownFrame(FrameHeader header, this.data) : super(header); + + @override + Map toJson() => super.toJson() + ..addAll({ + 'data (length)': data.length, + }); +}
diff --git a/http2/lib/src/frames/frame_utils.dart b/http2/lib/src/frames/frame_utils.dart new file mode 100644 index 0000000..73a3173 --- /dev/null +++ b/http2/lib/src/frames/frame_utils.dart
@@ -0,0 +1,7 @@ +// 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. + +part of http2.src.frames; + +bool _isFlagSet(int value, int flag) => value & flag == flag;
diff --git a/http2/lib/src/frames/frame_writer.dart b/http2/lib/src/frames/frame_writer.dart new file mode 100644 index 0000000..211aa54 --- /dev/null +++ b/http2/lib/src/frames/frame_writer.dart
@@ -0,0 +1,292 @@ +// 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. + +part of http2.src.frames; + +// TODO: No support for writing padded information. +// TODO: No support for stream priorities. +class FrameWriter { + /// The HPack compression context. + final HPackEncoder _hpackEncoder; + + /// A buffered writer for outgoing bytes. + final BufferedBytesWriter _outWriter; + + /// Connection settings which this writer needs to respect. + final ActiveSettings _peerSettings; + + /// This is the maximum over all stream id's we've written to the underlying + /// sink. + int _highestWrittenStreamId = 0; + + FrameWriter( + this._hpackEncoder, StreamSink<List<int>> outgoing, this._peerSettings) + : _outWriter = BufferedBytesWriter(outgoing); + + /// A indicator whether writes would be buffered. + BufferIndicator get bufferIndicator => _outWriter.bufferIndicator; + + /// This is the maximum over all stream id's we've written to the underlying + /// sink. + int get highestWrittenStreamId => _highestWrittenStreamId; + + void writeDataFrame(int streamId, List<int> data, {bool endStream = false}) { + while (data.length > _peerSettings.maxFrameSize) { + var chunk = viewOrSublist(data, 0, _peerSettings.maxFrameSize); + data = viewOrSublist(data, _peerSettings.maxFrameSize, + data.length - _peerSettings.maxFrameSize); + _writeDataFrameNoFragment(streamId, chunk, false); + } + _writeDataFrameNoFragment(streamId, data, endStream); + } + + void _writeDataFrameNoFragment(int streamId, List<int> data, bool endStream) { + var type = FrameType.DATA; + var flags = endStream ? DataFrame.FLAG_END_STREAM : 0; + + var buffer = Uint8List(FRAME_HEADER_SIZE + data.length); + var offset = 0; + + _setFrameHeader(buffer, offset, type, flags, streamId, data.length); + offset += FRAME_HEADER_SIZE; + + buffer.setRange(offset, offset + data.length, data); + + _writeData(buffer); + } + + void writeHeadersFrame(int streamId, List<Header> headers, + {bool endStream = true}) { + var fragment = _hpackEncoder.encode(headers); + var maxSize = + _peerSettings.maxFrameSize - HeadersFrame.MAX_CONSTANT_PAYLOAD; + + if (fragment.length < maxSize) { + _writeHeadersFrameNoFragment(streamId, fragment, true, endStream); + } else { + var chunk = fragment.sublist(0, maxSize); + fragment = fragment.sublist(maxSize); + _writeHeadersFrameNoFragment(streamId, chunk, false, endStream); + while (fragment.length > _peerSettings.maxFrameSize) { + var chunk = fragment.sublist(0, _peerSettings.maxFrameSize); + fragment = fragment.sublist(_peerSettings.maxFrameSize); + _writeContinuationFrame(streamId, chunk, false); + } + _writeContinuationFrame(streamId, fragment, true); + } + } + + void _writeHeadersFrameNoFragment( + int streamId, List<int> fragment, bool endHeaders, bool endStream) { + var type = FrameType.HEADERS; + var flags = 0; + if (endHeaders) flags |= HeadersFrame.FLAG_END_HEADERS; + if (endStream) flags |= HeadersFrame.FLAG_END_STREAM; + + var buffer = Uint8List(FRAME_HEADER_SIZE + fragment.length); + var offset = 0; + + _setFrameHeader(buffer, offset, type, flags, streamId, fragment.length); + offset += FRAME_HEADER_SIZE; + + buffer.setRange(offset, buffer.length, fragment); + + _writeData(buffer); + } + + void _writeContinuationFrame( + int streamId, List<int> fragment, bool endHeaders) { + var type = FrameType.CONTINUATION; + var flags = endHeaders ? ContinuationFrame.FLAG_END_HEADERS : 0; + + var buffer = Uint8List(FRAME_HEADER_SIZE + fragment.length); + var offset = 0; + + _setFrameHeader(buffer, offset, type, flags, streamId, fragment.length); + offset += FRAME_HEADER_SIZE; + + buffer.setRange(offset, buffer.length, fragment); + + _writeData(buffer); + } + + void writePriorityFrame(int streamId, int streamDependency, int weight, + {bool exclusive = false}) { + var type = FrameType.PRIORITY; + var flags = 0; + + var buffer = + Uint8List(FRAME_HEADER_SIZE + PriorityFrame.FIXED_FRAME_LENGTH); + var offset = 0; + + _setFrameHeader(buffer, offset, type, flags, streamId, 5); + offset += FRAME_HEADER_SIZE; + + if (exclusive) { + setInt32(buffer, offset, (1 << 31) | streamDependency); + } else { + setInt32(buffer, offset, streamDependency); + } + buffer[offset + 4] = weight; + + _writeData(buffer); + } + + void writeRstStreamFrame(int streamId, int errorCode) { + var type = FrameType.RST_STREAM; + var flags = 0; + + var buffer = + Uint8List(FRAME_HEADER_SIZE + RstStreamFrame.FIXED_FRAME_LENGTH); + var offset = 0; + + _setFrameHeader(buffer, offset, type, flags, streamId, 4); + offset += FRAME_HEADER_SIZE; + + setInt32(buffer, offset, errorCode); + + _writeData(buffer); + } + + void writeSettingsFrame(List<Setting> settings) { + var type = FrameType.SETTINGS; + var flags = 0; + + var buffer = Uint8List(FRAME_HEADER_SIZE + 6 * settings.length); + var offset = 0; + + _setFrameHeader(buffer, offset, type, flags, 0, 6 * settings.length); + offset += FRAME_HEADER_SIZE; + + for (var i = 0; i < settings.length; i++) { + var setting = settings[i]; + setInt16(buffer, offset + 6 * i, setting.identifier); + setInt32(buffer, offset + 6 * i + 2, setting.value); + } + + _writeData(buffer); + } + + void writeSettingsAckFrame() { + var type = FrameType.SETTINGS; + var flags = SettingsFrame.FLAG_ACK; + + var buffer = Uint8List(FRAME_HEADER_SIZE); + var offset = 0; + + _setFrameHeader(buffer, offset, type, flags, 0, 0); + offset += FRAME_HEADER_SIZE; + + _writeData(buffer); + } + + void writePushPromiseFrame( + int streamId, int promisedStreamId, List<Header> headers) { + var fragment = _hpackEncoder.encode(headers); + var maxSize = + _peerSettings.maxFrameSize - PushPromiseFrame.MAX_CONSTANT_PAYLOAD; + + if (fragment.length < maxSize) { + _writePushPromiseFrameNoFragmentation( + streamId, promisedStreamId, fragment, true); + } else { + var chunk = fragment.sublist(0, maxSize); + fragment = fragment.sublist(maxSize); + _writePushPromiseFrameNoFragmentation( + streamId, promisedStreamId, chunk, false); + while (fragment.length > _peerSettings.maxFrameSize) { + var chunk = fragment.sublist(0, _peerSettings.maxFrameSize); + fragment = fragment.sublist(_peerSettings.maxFrameSize); + _writeContinuationFrame(streamId, chunk, false); + } + _writeContinuationFrame(streamId, chunk, true); + } + } + + void _writePushPromiseFrameNoFragmentation( + int streamId, int promisedStreamId, List<int> fragment, bool endHeaders) { + var type = FrameType.PUSH_PROMISE; + var flags = endHeaders ? HeadersFrame.FLAG_END_HEADERS : 0; + + var buffer = Uint8List(FRAME_HEADER_SIZE + 4 + fragment.length); + var offset = 0; + + _setFrameHeader(buffer, offset, type, flags, streamId, 4 + fragment.length); + offset += FRAME_HEADER_SIZE; + + setInt32(buffer, offset, promisedStreamId); + buffer.setRange(offset + 4, offset + 4 + fragment.length, fragment); + + _writeData(buffer); + } + + void writePingFrame(int opaqueData, {bool ack = false}) { + var type = FrameType.PING; + var flags = ack ? PingFrame.FLAG_ACK : 0; + + var buffer = Uint8List(FRAME_HEADER_SIZE + PingFrame.FIXED_FRAME_LENGTH); + var offset = 0; + + _setFrameHeader(buffer, 0, type, flags, 0, 8); + offset += FRAME_HEADER_SIZE; + + setInt64(buffer, offset, opaqueData); + _writeData(buffer); + } + + void writeGoawayFrame(int lastStreamId, int errorCode, List<int> debugData) { + var type = FrameType.GOAWAY; + var flags = 0; + + var buffer = Uint8List(FRAME_HEADER_SIZE + 8 + debugData.length); + var offset = 0; + + _setFrameHeader(buffer, offset, type, flags, 0, 8 + debugData.length); + offset += FRAME_HEADER_SIZE; + + setInt32(buffer, offset, lastStreamId); + setInt32(buffer, offset + 4, errorCode); + buffer.setRange(offset + 8, buffer.length, debugData); + + _writeData(buffer); + } + + void writeWindowUpdate(int sizeIncrement, {int streamId = 0}) { + var type = FrameType.WINDOW_UPDATE; + var flags = 0; + + var buffer = + Uint8List(FRAME_HEADER_SIZE + WindowUpdateFrame.FIXED_FRAME_LENGTH); + var offset = 0; + + _setFrameHeader(buffer, offset, type, flags, streamId, 4); + offset += FRAME_HEADER_SIZE; + + setInt32(buffer, offset, sizeIncrement); + + _writeData(buffer); + } + + void _writeData(List<int> bytes) { + _outWriter.add(bytes); + } + + /// Closes the underlying sink and returns [doneFuture]. + Future close() { + return _outWriter.close().whenComplete(() => doneFuture); + } + + /// The future which will complete once this writer is done. + Future get doneFuture => _outWriter.doneFuture; + + void _setFrameHeader(List<int> bytes, int offset, int type, int flags, + int streamId, int length) { + setInt24(bytes, offset, length); + bytes[3] = type; + bytes[4] = flags; + setInt32(bytes, 5, streamId); + + _highestWrittenStreamId = max(_highestWrittenStreamId, streamId); + } +}
diff --git a/http2/lib/src/frames/frames.dart b/http2/lib/src/frames/frames.dart new file mode 100644 index 0000000..f6e74d2 --- /dev/null +++ b/http2/lib/src/frames/frames.dart
@@ -0,0 +1,20 @@ +// 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. + +library http2.src.frames; + +import 'dart:async'; +import 'dart:math' show max; +import 'dart:typed_data'; + +import '../async_utils/async_utils.dart'; +import '../byte_utils.dart'; +import '../hpack/hpack.dart'; +import '../settings/settings.dart'; +import '../sync_errors.dart'; + +part 'frame_types.dart'; +part 'frame_utils.dart'; +part 'frame_reader.dart'; +part 'frame_writer.dart';
diff --git a/http2/lib/src/hpack/hpack.dart b/http2/lib/src/hpack/hpack.dart new file mode 100644 index 0000000..62338a1 --- /dev/null +++ b/http2/lib/src/hpack/hpack.dart
@@ -0,0 +1,344 @@ +// 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. + +/// Implements a [HPackContext] for encoding/decoding headers according to the +/// HPACK specificaiton. See here for more information: +/// https://tools.ietf.org/html/draft-ietf-httpbis-header-compression-10 +library http2.hpack; + +import 'dart:convert' show ascii; +import 'dart:io'; + +import '../byte_utils.dart'; + +import 'huffman.dart'; +import 'huffman_table.dart'; + +/// Exception raised due to encoding/decoding errors. +class HPackDecodingException implements Exception { + final String _message; + + HPackDecodingException(this._message); + + @override + String toString() => 'HPackDecodingException: $_message'; +} + +/// A HPACK encoding/decoding context. +/// +/// This is a statefull class, so encoding/decoding changes internal state. +class HPackContext { + final HPackEncoder encoder = HPackEncoder(); + final HPackDecoder decoder = HPackDecoder(); + + HPackContext( + {int maxSendingHeaderTableSize = 4096, + int maxReceivingHeaderTableSize = 4096}) { + encoder.updateMaxSendingHeaderTableSize(maxSendingHeaderTableSize); + decoder.updateMaxReceivingHeaderTableSize(maxReceivingHeaderTableSize); + } +} + +/// A HTTP/2 header. +class Header { + final List<int> name; + final List<int> value; + final bool neverIndexed; + + Header(this.name, this.value, {this.neverIndexed = false}); + + factory Header.ascii(String name, String value) { + return Header(ascii.encode(name), ascii.encode(value)); + } +} + +/// A stateful HPACK decoder. +class HPackDecoder { + late int _maxHeaderTableSize; + + final IndexTable _table = IndexTable(); + + void updateMaxReceivingHeaderTableSize(int newMaximumSize) { + _maxHeaderTableSize = newMaximumSize; + } + + List<Header> decode(List<int> data) { + var offset = 0; + + int readInteger(int prefixBits) { + assert(prefixBits <= 8 && prefixBits > 0); + + var byte = data[offset++] & ((1 << prefixBits) - 1); + + int integer; + if (byte == ((1 << prefixBits) - 1)) { + // Length encodeded. + integer = 0; + var shift = 0; + while (true) { + var done = (data[offset] & 0x80) != 0x80; + integer += (data[offset++] & 0x7f) << shift; + shift += 7; + if (done) break; + } + integer += (1 << prefixBits) - 1; + } else { + // In place length. + integer = byte; + } + + return integer; + } + + List<int> readStringLiteral() { + var isHuffmanEncoding = (data[offset] & 0x80) != 0; + var length = readInteger(7); + + var sublist = viewOrSublist(data, offset, length); + offset += length; + if (isHuffmanEncoding) { + return http2HuffmanCodec.decode(sublist); + } else { + return sublist; + } + } + + Header readHeaderFieldInternal(int index, {bool neverIndexed = false}) { + List<int> name, value; + if (index > 0) { + name = _table.lookup(index).name; + value = readStringLiteral(); + } else { + name = readStringLiteral(); + value = readStringLiteral(); + } + return Header(name, value, neverIndexed: neverIndexed); + } + + try { + var headers = <Header>[]; + while (offset < data.length) { + var byte = data[offset]; + var isIndexedField = (byte & 0x80) != 0; + var isIncrementalIndexing = (byte & 0xc0) == 0x40; + + var isWithoutIndexing = (byte & 0xf0) == 0; + var isNeverIndexing = (byte & 0xf0) == 0x10; + var isDynamicTableSizeUpdate = (byte & 0xe0) == 0x20; + + if (isIndexedField) { + var index = readInteger(7); + var field = _table.lookup(index); + headers.add(field); + } else if (isIncrementalIndexing) { + var field = readHeaderFieldInternal(readInteger(6)); + _table.addHeaderField(field); + headers.add(field); + } else if (isWithoutIndexing) { + headers.add(readHeaderFieldInternal(readInteger(4))); + } else if (isNeverIndexing) { + headers + .add(readHeaderFieldInternal(readInteger(4), neverIndexed: true)); + } else if (isDynamicTableSizeUpdate) { + var newMaxSize = readInteger(5); + if (newMaxSize <= _maxHeaderTableSize) { + _table.updateMaxSize(newMaxSize); + } else { + throw HPackDecodingException('Dynamic table size update failed: ' + 'A new value of $newMaxSize exceeds the limit of ' + '$_maxHeaderTableSize'); + } + } else { + throw HPackDecodingException('Invalid encoding of headers.'); + } + } + return headers; + } on RangeError catch (e) { + throw HPackDecodingException('$e'); + } on HuffmanDecodingException catch (e) { + throw HPackDecodingException('$e'); + } + } +} + +/// A stateful HPACK encoder. +// TODO: Currently we encode all headers: +// - without huffman encoding +// - without using the dynamic table +class HPackEncoder { + void updateMaxSendingHeaderTableSize(int newMaximumSize) { + // TODO: Once we start encoding via dynamic table we need to let the other + // side know the maximum table size we're using. + } + + List<int> encode(List<Header> headers) { + var bytesBuilder = BytesBuilder(); + var currentByte = 0; + + void writeInteger(int prefixBits, int value) { + assert(prefixBits <= 8); + + if (value < (1 << prefixBits) - 1) { + currentByte |= value; + bytesBuilder.addByte(currentByte); + } else { + // Length encodeded. + currentByte |= (1 << prefixBits) - 1; + value -= (1 << prefixBits) - 1; + bytesBuilder.addByte(currentByte); + var done = false; + while (!done) { + currentByte = value & 0x7f; + value = value >> 7; + done = value == 0; + if (!done) currentByte |= 0x80; + bytesBuilder.addByte(currentByte); + } + } + currentByte = 0; + } + + void writeStringLiteral(List<int> bytes) { + // TODO: Support huffman encoding. + currentByte = 0; // 1 would be huffman encoding + writeInteger(7, bytes.length); + bytesBuilder.add(bytes); + } + + void writeLiteralHeaderWithoutIndexing(Header header) { + bytesBuilder.addByte(0); + writeStringLiteral(header.name); + writeStringLiteral(header.value); + } + + for (var header in headers) { + writeLiteralHeaderWithoutIndexing(header); + } + + return bytesBuilder.takeBytes(); + } +} + +class IndexTable { + static final List<Header?> _staticTable = [ + null, + Header(ascii.encode(':authority'), const []), + Header(ascii.encode(':method'), ascii.encode('GET')), + Header(ascii.encode(':method'), ascii.encode('POST')), + Header(ascii.encode(':path'), ascii.encode('/')), + Header(ascii.encode(':path'), ascii.encode('/index.html')), + Header(ascii.encode(':scheme'), ascii.encode('http')), + Header(ascii.encode(':scheme'), ascii.encode('https')), + Header(ascii.encode(':status'), ascii.encode('200')), + Header(ascii.encode(':status'), ascii.encode('204')), + Header(ascii.encode(':status'), ascii.encode('206')), + Header(ascii.encode(':status'), ascii.encode('304')), + Header(ascii.encode(':status'), ascii.encode('400')), + Header(ascii.encode(':status'), ascii.encode('404')), + Header(ascii.encode(':status'), ascii.encode('500')), + Header(ascii.encode('accept-charset'), const []), + Header(ascii.encode('accept-encoding'), ascii.encode('gzip, deflate')), + Header(ascii.encode('accept-language'), const []), + Header(ascii.encode('accept-ranges'), const []), + Header(ascii.encode('accept'), const []), + Header(ascii.encode('access-control-allow-origin'), const []), + Header(ascii.encode('age'), const []), + Header(ascii.encode('allow'), const []), + Header(ascii.encode('authorization'), const []), + Header(ascii.encode('cache-control'), const []), + Header(ascii.encode('content-disposition'), const []), + Header(ascii.encode('content-encoding'), const []), + Header(ascii.encode('content-language'), const []), + Header(ascii.encode('content-length'), const []), + Header(ascii.encode('content-location'), const []), + Header(ascii.encode('content-range'), const []), + Header(ascii.encode('content-type'), const []), + Header(ascii.encode('cookie'), const []), + Header(ascii.encode('date'), const []), + Header(ascii.encode('etag'), const []), + Header(ascii.encode('expect'), const []), + Header(ascii.encode('expires'), const []), + Header(ascii.encode('from'), const []), + Header(ascii.encode('host'), const []), + Header(ascii.encode('if-match'), const []), + Header(ascii.encode('if-modified-since'), const []), + Header(ascii.encode('if-none-match'), const []), + Header(ascii.encode('if-range'), const []), + Header(ascii.encode('if-unmodified-since'), const []), + Header(ascii.encode('last-modified'), const []), + Header(ascii.encode('link'), const []), + Header(ascii.encode('location'), const []), + Header(ascii.encode('max-forwards'), const []), + Header(ascii.encode('proxy-authenticate'), const []), + Header(ascii.encode('proxy-authorization'), const []), + Header(ascii.encode('range'), const []), + Header(ascii.encode('referer'), const []), + Header(ascii.encode('refresh'), const []), + Header(ascii.encode('retry-after'), const []), + Header(ascii.encode('server'), const []), + Header(ascii.encode('set-cookie'), const []), + Header(ascii.encode('strict-transport-security'), const []), + Header(ascii.encode('transfer-encoding'), const []), + Header(ascii.encode('user-agent'), const []), + Header(ascii.encode('vary'), const []), + Header(ascii.encode('via'), const []), + Header(ascii.encode('www-authenticate'), const []), + ]; + + final List<Header> _dynamicTable = []; + + /// The maximum size the dynamic table can grow to before entries need to be + /// evicted. + int _maximumSize = 4096; + + /// The current size of the dynamic table. + int _currentSize = 0; + + IndexTable(); + + /// Updates the maximum size which the dynamic table can grow to. + void updateMaxSize(int newMaxDynTableSize) { + _maximumSize = newMaxDynTableSize; + _reduce(); + } + + /// Lookup an item by index. + Header lookup(int index) { + if (index <= 0) { + throw HPackDecodingException( + 'Invalid index (was: $index) for table lookup.'); + } + if (index < _staticTable.length) { + return _staticTable[index]!; + } + index -= _staticTable.length; + if (index < _dynamicTable.length) { + return _dynamicTable[index]; + } + throw HPackDecodingException( + 'Invalid index (was: $index) for table lookup.'); + } + + /// Adds a new header field to the dynamic table - and evicts entries as + /// necessary. + void addHeaderField(Header header) { + _dynamicTable.insert(0, header); + _currentSize += _sizeOf(header); + _reduce(); + } + + /// Removes as many entries as required to be within the limit of + /// [_maximumSize]. + void _reduce() { + while (_currentSize > _maximumSize) { + var h = _dynamicTable.removeLast(); + _currentSize -= _sizeOf(h); + } + } + + /// Returns the "size" a [header] has. + /// + /// This is specified to be the number of octets of name/value plus 32. + int _sizeOf(Header header) => header.name.length + header.value.length + 32; +}
diff --git a/http2/lib/src/hpack/huffman.dart b/http2/lib/src/hpack/huffman.dart new file mode 100644 index 0000000..11b5a0a --- /dev/null +++ b/http2/lib/src/hpack/huffman.dart
@@ -0,0 +1,180 @@ +// 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:io'; + +import 'huffman_table.dart'; + +class HuffmanDecodingException implements Exception { + final String _message; + + HuffmanDecodingException(this._message); + + @override + String toString() => 'HuffmanDecodingException: $_message'; +} + +/// A codec used for encoding/decoding using a huffman codec. +class HuffmanCodec { + final HuffmanEncoder _encoder; + final HuffmanDecoder _decoder; + + HuffmanCodec(this._encoder, this._decoder); + + List<int> decode(List<int> bytes) => _decoder.decode(bytes); + + List<int> encode(List<int> bytes) => _encoder.encode(bytes); +} + +/// A huffman decoder based on a [HuffmanTreeNode]. +class HuffmanDecoder { + final HuffmanTreeNode _root; + + HuffmanDecoder(this._root); + + /// Decodes [bytes] using a huffman tree. + List<int> decode(List<int> bytes) { + var buffer = BytesBuilder(); + + var currentByteOffset = 0; + var node = _root; + var currentDepth = 0; + while (currentByteOffset < bytes.length) { + var byte = bytes[currentByteOffset]; + for (var currentBit = 7; currentBit >= 0; currentBit--) { + var right = (byte >> currentBit) & 1 == 1; + if (right) { + node = node.right!; + } else { + node = node.left!; + } + currentDepth++; + if (node.value != null) { + if (node.value == EOS_BYTE) { + throw HuffmanDecodingException( + 'More than 7 bit padding is not allowed. Found entire EOS ' + 'encoding'); + } + buffer.addByte(node.value!); + node = _root; + currentDepth = 0; + } + } + currentByteOffset++; + } + + if (node != _root) { + if (currentDepth > 7) { + throw HuffmanDecodingException( + 'Incomplete encoding of a byte or more than 7 bit padding.'); + } + + while (node.right != null) { + node = node.right!; + } + + if (node.value != 256) { + throw HuffmanDecodingException('Incomplete encoding of a byte.'); + } + } + + return buffer.takeBytes(); + } +} + +/// A huffman encoder based on a list of codewords. +class HuffmanEncoder { + final List<EncodedHuffmanValue> _codewords; + + HuffmanEncoder(this._codewords); + + /// Encodes [bytes] using a list of codewords. + List<int> encode(List<int> bytes) { + var buffer = BytesBuilder(); + + var currentByte = 0; + var currentBitOffset = 7; + + void writeValue(int value, int numBits) { + var i = numBits - 1; + while (i >= 0) { + if (currentBitOffset == 7 && i >= 7) { + assert(currentByte == 0); + + buffer.addByte((value >> (i - 7)) & 0xff); + currentBitOffset = 7; + currentByte = 0; + i -= 8; + } else { + currentByte |= ((value >> i) & 1) << currentBitOffset; + + currentBitOffset--; + if (currentBitOffset == -1) { + buffer.addByte(currentByte); + currentBitOffset = 7; + currentByte = 0; + } + i--; + } + } + } + + for (var i = 0; i < bytes.length; i++) { + var byte = bytes[i]; + var value = _codewords[byte]; + writeValue(value.encodedBytes, value.numBits); + } + + if (currentBitOffset < 7) { + writeValue(0xff, 1 + currentBitOffset); + } + + return buffer.takeBytes(); + } +} + +/// Specifies the encoding of a specific value using huffman encoding. +class EncodedHuffmanValue { + /// An integer representation of the encoded bit-string. + final int encodedBytes; + + /// The number of bits in [encodedBytes]. + final int numBits; + + const EncodedHuffmanValue(this.encodedBytes, this.numBits); +} + +/// A node in the huffman tree. +class HuffmanTreeNode { + HuffmanTreeNode? left; + HuffmanTreeNode? right; + int? value; +} + +/// Generates a huffman decoding tree. +HuffmanTreeNode generateHuffmanTree(List<EncodedHuffmanValue> valueEncodings) { + var root = HuffmanTreeNode(); + + for (var byteOffset = 0; byteOffset < valueEncodings.length; byteOffset++) { + var entry = valueEncodings[byteOffset]; + + var current = root; + for (var bitNr = 0; bitNr < entry.numBits; bitNr++) { + var right = + ((entry.encodedBytes >> (entry.numBits - bitNr - 1)) & 1) == 1; + + if (right) { + current.right ??= HuffmanTreeNode(); + current = current.right!; + } else { + current.left ??= HuffmanTreeNode(); + current = current.left!; + } + } + + current.value = byteOffset; + } + + return root; +}
diff --git a/http2/lib/src/hpack/huffman_table.dart b/http2/lib/src/hpack/huffman_table.dart new file mode 100644 index 0000000..488e124 --- /dev/null +++ b/http2/lib/src/hpack/huffman_table.dart
@@ -0,0 +1,275 @@ +// 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 'huffman.dart'; + +/// The huffman codec for encoding/decoding HTTP/2 header blocks. +final HuffmanCodec http2HuffmanCodec = HuffmanCodec(HuffmanEncoder(_codeWords), + HuffmanDecoder(generateHuffmanTree(_codeWords))); + +/// This is the integer representing the End-of-String symbol +/// (it is not representable by a byte). +const int EOS_BYTE = 256; + +/// This list of byte encodings via huffman encoding was generated from the +/// HPACK specification. +const List<EncodedHuffmanValue> _codeWords = <EncodedHuffmanValue>[ + EncodedHuffmanValue(0x1ff8, 13), + EncodedHuffmanValue(0x7fffd8, 23), + EncodedHuffmanValue(0xfffffe2, 28), + EncodedHuffmanValue(0xfffffe3, 28), + EncodedHuffmanValue(0xfffffe4, 28), + EncodedHuffmanValue(0xfffffe5, 28), + EncodedHuffmanValue(0xfffffe6, 28), + EncodedHuffmanValue(0xfffffe7, 28), + EncodedHuffmanValue(0xfffffe8, 28), + EncodedHuffmanValue(0xffffea, 24), + EncodedHuffmanValue(0x3ffffffc, 30), + EncodedHuffmanValue(0xfffffe9, 28), + EncodedHuffmanValue(0xfffffea, 28), + EncodedHuffmanValue(0x3ffffffd, 30), + EncodedHuffmanValue(0xfffffeb, 28), + EncodedHuffmanValue(0xfffffec, 28), + EncodedHuffmanValue(0xfffffed, 28), + EncodedHuffmanValue(0xfffffee, 28), + EncodedHuffmanValue(0xfffffef, 28), + EncodedHuffmanValue(0xffffff0, 28), + EncodedHuffmanValue(0xffffff1, 28), + EncodedHuffmanValue(0xffffff2, 28), + EncodedHuffmanValue(0x3ffffffe, 30), + EncodedHuffmanValue(0xffffff3, 28), + EncodedHuffmanValue(0xffffff4, 28), + EncodedHuffmanValue(0xffffff5, 28), + EncodedHuffmanValue(0xffffff6, 28), + EncodedHuffmanValue(0xffffff7, 28), + EncodedHuffmanValue(0xffffff8, 28), + EncodedHuffmanValue(0xffffff9, 28), + EncodedHuffmanValue(0xffffffa, 28), + EncodedHuffmanValue(0xffffffb, 28), + EncodedHuffmanValue(0x14, 6), + EncodedHuffmanValue(0x3f8, 10), + EncodedHuffmanValue(0x3f9, 10), + EncodedHuffmanValue(0xffa, 12), + EncodedHuffmanValue(0x1ff9, 13), + EncodedHuffmanValue(0x15, 6), + EncodedHuffmanValue(0xf8, 8), + EncodedHuffmanValue(0x7fa, 11), + EncodedHuffmanValue(0x3fa, 10), + EncodedHuffmanValue(0x3fb, 10), + EncodedHuffmanValue(0xf9, 8), + EncodedHuffmanValue(0x7fb, 11), + EncodedHuffmanValue(0xfa, 8), + EncodedHuffmanValue(0x16, 6), + EncodedHuffmanValue(0x17, 6), + EncodedHuffmanValue(0x18, 6), + EncodedHuffmanValue(0x0, 5), + EncodedHuffmanValue(0x1, 5), + EncodedHuffmanValue(0x2, 5), + EncodedHuffmanValue(0x19, 6), + EncodedHuffmanValue(0x1a, 6), + EncodedHuffmanValue(0x1b, 6), + EncodedHuffmanValue(0x1c, 6), + EncodedHuffmanValue(0x1d, 6), + EncodedHuffmanValue(0x1e, 6), + EncodedHuffmanValue(0x1f, 6), + EncodedHuffmanValue(0x5c, 7), + EncodedHuffmanValue(0xfb, 8), + EncodedHuffmanValue(0x7ffc, 15), + EncodedHuffmanValue(0x20, 6), + EncodedHuffmanValue(0xffb, 12), + EncodedHuffmanValue(0x3fc, 10), + EncodedHuffmanValue(0x1ffa, 13), + EncodedHuffmanValue(0x21, 6), + EncodedHuffmanValue(0x5d, 7), + EncodedHuffmanValue(0x5e, 7), + EncodedHuffmanValue(0x5f, 7), + EncodedHuffmanValue(0x60, 7), + EncodedHuffmanValue(0x61, 7), + EncodedHuffmanValue(0x62, 7), + EncodedHuffmanValue(0x63, 7), + EncodedHuffmanValue(0x64, 7), + EncodedHuffmanValue(0x65, 7), + EncodedHuffmanValue(0x66, 7), + EncodedHuffmanValue(0x67, 7), + EncodedHuffmanValue(0x68, 7), + EncodedHuffmanValue(0x69, 7), + EncodedHuffmanValue(0x6a, 7), + EncodedHuffmanValue(0x6b, 7), + EncodedHuffmanValue(0x6c, 7), + EncodedHuffmanValue(0x6d, 7), + EncodedHuffmanValue(0x6e, 7), + EncodedHuffmanValue(0x6f, 7), + EncodedHuffmanValue(0x70, 7), + EncodedHuffmanValue(0x71, 7), + EncodedHuffmanValue(0x72, 7), + EncodedHuffmanValue(0xfc, 8), + EncodedHuffmanValue(0x73, 7), + EncodedHuffmanValue(0xfd, 8), + EncodedHuffmanValue(0x1ffb, 13), + EncodedHuffmanValue(0x7fff0, 19), + EncodedHuffmanValue(0x1ffc, 13), + EncodedHuffmanValue(0x3ffc, 14), + EncodedHuffmanValue(0x22, 6), + EncodedHuffmanValue(0x7ffd, 15), + EncodedHuffmanValue(0x3, 5), + EncodedHuffmanValue(0x23, 6), + EncodedHuffmanValue(0x4, 5), + EncodedHuffmanValue(0x24, 6), + EncodedHuffmanValue(0x5, 5), + EncodedHuffmanValue(0x25, 6), + EncodedHuffmanValue(0x26, 6), + EncodedHuffmanValue(0x27, 6), + EncodedHuffmanValue(0x6, 5), + EncodedHuffmanValue(0x74, 7), + EncodedHuffmanValue(0x75, 7), + EncodedHuffmanValue(0x28, 6), + EncodedHuffmanValue(0x29, 6), + EncodedHuffmanValue(0x2a, 6), + EncodedHuffmanValue(0x7, 5), + EncodedHuffmanValue(0x2b, 6), + EncodedHuffmanValue(0x76, 7), + EncodedHuffmanValue(0x2c, 6), + EncodedHuffmanValue(0x8, 5), + EncodedHuffmanValue(0x9, 5), + EncodedHuffmanValue(0x2d, 6), + EncodedHuffmanValue(0x77, 7), + EncodedHuffmanValue(0x78, 7), + EncodedHuffmanValue(0x79, 7), + EncodedHuffmanValue(0x7a, 7), + EncodedHuffmanValue(0x7b, 7), + EncodedHuffmanValue(0x7ffe, 15), + EncodedHuffmanValue(0x7fc, 11), + EncodedHuffmanValue(0x3ffd, 14), + EncodedHuffmanValue(0x1ffd, 13), + EncodedHuffmanValue(0xffffffc, 28), + EncodedHuffmanValue(0xfffe6, 20), + EncodedHuffmanValue(0x3fffd2, 22), + EncodedHuffmanValue(0xfffe7, 20), + EncodedHuffmanValue(0xfffe8, 20), + EncodedHuffmanValue(0x3fffd3, 22), + EncodedHuffmanValue(0x3fffd4, 22), + EncodedHuffmanValue(0x3fffd5, 22), + EncodedHuffmanValue(0x7fffd9, 23), + EncodedHuffmanValue(0x3fffd6, 22), + EncodedHuffmanValue(0x7fffda, 23), + EncodedHuffmanValue(0x7fffdb, 23), + EncodedHuffmanValue(0x7fffdc, 23), + EncodedHuffmanValue(0x7fffdd, 23), + EncodedHuffmanValue(0x7fffde, 23), + EncodedHuffmanValue(0xffffeb, 24), + EncodedHuffmanValue(0x7fffdf, 23), + EncodedHuffmanValue(0xffffec, 24), + EncodedHuffmanValue(0xffffed, 24), + EncodedHuffmanValue(0x3fffd7, 22), + EncodedHuffmanValue(0x7fffe0, 23), + EncodedHuffmanValue(0xffffee, 24), + EncodedHuffmanValue(0x7fffe1, 23), + EncodedHuffmanValue(0x7fffe2, 23), + EncodedHuffmanValue(0x7fffe3, 23), + EncodedHuffmanValue(0x7fffe4, 23), + EncodedHuffmanValue(0x1fffdc, 21), + EncodedHuffmanValue(0x3fffd8, 22), + EncodedHuffmanValue(0x7fffe5, 23), + EncodedHuffmanValue(0x3fffd9, 22), + EncodedHuffmanValue(0x7fffe6, 23), + EncodedHuffmanValue(0x7fffe7, 23), + EncodedHuffmanValue(0xffffef, 24), + EncodedHuffmanValue(0x3fffda, 22), + EncodedHuffmanValue(0x1fffdd, 21), + EncodedHuffmanValue(0xfffe9, 20), + EncodedHuffmanValue(0x3fffdb, 22), + EncodedHuffmanValue(0x3fffdc, 22), + EncodedHuffmanValue(0x7fffe8, 23), + EncodedHuffmanValue(0x7fffe9, 23), + EncodedHuffmanValue(0x1fffde, 21), + EncodedHuffmanValue(0x7fffea, 23), + EncodedHuffmanValue(0x3fffdd, 22), + EncodedHuffmanValue(0x3fffde, 22), + EncodedHuffmanValue(0xfffff0, 24), + EncodedHuffmanValue(0x1fffdf, 21), + EncodedHuffmanValue(0x3fffdf, 22), + EncodedHuffmanValue(0x7fffeb, 23), + EncodedHuffmanValue(0x7fffec, 23), + EncodedHuffmanValue(0x1fffe0, 21), + EncodedHuffmanValue(0x1fffe1, 21), + EncodedHuffmanValue(0x3fffe0, 22), + EncodedHuffmanValue(0x1fffe2, 21), + EncodedHuffmanValue(0x7fffed, 23), + EncodedHuffmanValue(0x3fffe1, 22), + EncodedHuffmanValue(0x7fffee, 23), + EncodedHuffmanValue(0x7fffef, 23), + EncodedHuffmanValue(0xfffea, 20), + EncodedHuffmanValue(0x3fffe2, 22), + EncodedHuffmanValue(0x3fffe3, 22), + EncodedHuffmanValue(0x3fffe4, 22), + EncodedHuffmanValue(0x7ffff0, 23), + EncodedHuffmanValue(0x3fffe5, 22), + EncodedHuffmanValue(0x3fffe6, 22), + EncodedHuffmanValue(0x7ffff1, 23), + EncodedHuffmanValue(0x3ffffe0, 26), + EncodedHuffmanValue(0x3ffffe1, 26), + EncodedHuffmanValue(0xfffeb, 20), + EncodedHuffmanValue(0x7fff1, 19), + EncodedHuffmanValue(0x3fffe7, 22), + EncodedHuffmanValue(0x7ffff2, 23), + EncodedHuffmanValue(0x3fffe8, 22), + EncodedHuffmanValue(0x1ffffec, 25), + EncodedHuffmanValue(0x3ffffe2, 26), + EncodedHuffmanValue(0x3ffffe3, 26), + EncodedHuffmanValue(0x3ffffe4, 26), + EncodedHuffmanValue(0x7ffffde, 27), + EncodedHuffmanValue(0x7ffffdf, 27), + EncodedHuffmanValue(0x3ffffe5, 26), + EncodedHuffmanValue(0xfffff1, 24), + EncodedHuffmanValue(0x1ffffed, 25), + EncodedHuffmanValue(0x7fff2, 19), + EncodedHuffmanValue(0x1fffe3, 21), + EncodedHuffmanValue(0x3ffffe6, 26), + EncodedHuffmanValue(0x7ffffe0, 27), + EncodedHuffmanValue(0x7ffffe1, 27), + EncodedHuffmanValue(0x3ffffe7, 26), + EncodedHuffmanValue(0x7ffffe2, 27), + EncodedHuffmanValue(0xfffff2, 24), + EncodedHuffmanValue(0x1fffe4, 21), + EncodedHuffmanValue(0x1fffe5, 21), + EncodedHuffmanValue(0x3ffffe8, 26), + EncodedHuffmanValue(0x3ffffe9, 26), + EncodedHuffmanValue(0xffffffd, 28), + EncodedHuffmanValue(0x7ffffe3, 27), + EncodedHuffmanValue(0x7ffffe4, 27), + EncodedHuffmanValue(0x7ffffe5, 27), + EncodedHuffmanValue(0xfffec, 20), + EncodedHuffmanValue(0xfffff3, 24), + EncodedHuffmanValue(0xfffed, 20), + EncodedHuffmanValue(0x1fffe6, 21), + EncodedHuffmanValue(0x3fffe9, 22), + EncodedHuffmanValue(0x1fffe7, 21), + EncodedHuffmanValue(0x1fffe8, 21), + EncodedHuffmanValue(0x7ffff3, 23), + EncodedHuffmanValue(0x3fffea, 22), + EncodedHuffmanValue(0x3fffeb, 22), + EncodedHuffmanValue(0x1ffffee, 25), + EncodedHuffmanValue(0x1ffffef, 25), + EncodedHuffmanValue(0xfffff4, 24), + EncodedHuffmanValue(0xfffff5, 24), + EncodedHuffmanValue(0x3ffffea, 26), + EncodedHuffmanValue(0x7ffff4, 23), + EncodedHuffmanValue(0x3ffffeb, 26), + EncodedHuffmanValue(0x7ffffe6, 27), + EncodedHuffmanValue(0x3ffffec, 26), + EncodedHuffmanValue(0x3ffffed, 26), + EncodedHuffmanValue(0x7ffffe7, 27), + EncodedHuffmanValue(0x7ffffe8, 27), + EncodedHuffmanValue(0x7ffffe9, 27), + EncodedHuffmanValue(0x7ffffea, 27), + EncodedHuffmanValue(0x7ffffeb, 27), + EncodedHuffmanValue(0xffffffe, 28), + EncodedHuffmanValue(0x7ffffec, 27), + EncodedHuffmanValue(0x7ffffed, 27), + EncodedHuffmanValue(0x7ffffee, 27), + EncodedHuffmanValue(0x7ffffef, 27), + EncodedHuffmanValue(0x7fffff0, 27), + EncodedHuffmanValue(0x3ffffee, 26), + EncodedHuffmanValue(0x3fffffff, 30), +];
diff --git a/http2/lib/src/ping/ping_handler.dart b/http2/lib/src/ping/ping_handler.dart new file mode 100644 index 0000000..7682e7e --- /dev/null +++ b/http2/lib/src/ping/ping_handler.dart
@@ -0,0 +1,62 @@ +// 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 '../error_handler.dart'; +import '../frames/frames.dart'; +import '../sync_errors.dart'; + +/// Responsible for pinging the other end and for handling pings from the +/// other end. +// TODO: We currently write unconditionally to the [FrameWriter]: we might want +// to consider be more aware what [Framewriter.bufferIndicator.wouldBuffer] +// says. +class PingHandler extends Object with TerminatableMixin { + final FrameWriter _frameWriter; + final Map<int, Completer> _remainingPings = {}; + int _nextId = 1; + + PingHandler(this._frameWriter); + + @override + void onTerminated(Object? error) { + var values = _remainingPings.values.toList(); + _remainingPings.clear(); + values.forEach( + (Completer c) => c.completeError(error ?? 'Unspecified error')); + } + + void processPingFrame(PingFrame frame) { + ensureNotTerminatedSync(() { + if (frame.header.streamId != 0) { + throw ProtocolException('Ping frames must have a stream id of 0.'); + } + + if (!frame.hasAckFlag) { + _frameWriter.writePingFrame(frame.opaqueData, ack: true); + } else { + var c = _remainingPings.remove(frame.opaqueData); + if (c != null) { + c.complete(); + } else { + // NOTE: It is not specified what happens when one gets an ACK for a + // ping we never sent. We be very strict and fail in this case. + throw ProtocolException( + 'Received ping ack with unknown opaque data.'); + } + } + }); + } + + Future ping() { + return ensureNotTerminatedAsync(() { + var c = Completer(); + var id = _nextId++; + _remainingPings[id] = c; + _frameWriter.writePingFrame(id); + return c.future; + }); + } +}
diff --git a/http2/lib/src/settings/settings.dart b/http2/lib/src/settings/settings.dart new file mode 100644 index 0000000..19d48a0 --- /dev/null +++ b/http2/lib/src/settings/settings.dart
@@ -0,0 +1,224 @@ +// 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 '../error_handler.dart'; +import '../frames/frames.dart'; +import '../hpack/hpack.dart'; +import '../sync_errors.dart'; + +/// The settings a remote peer can choose to set. +class ActiveSettings { + /// Allows the sender to inform the remote endpoint of the maximum size of the + /// header compression table used to decode header blocks, in octets. The + /// encoder can select any size equal to or less than this value by using + /// signaling specific to the header compression format inside a header block. + /// The initial value is 4,096 octets. + int headerTableSize; + + /// This setting can be use to disable server push (Section 8.2). An endpoint + /// MUST NOT send a PUSH_PROMISE frame if it receives this parameter set to a + /// value of 0. An endpoint that has both set this parameter to 0 and had it + /// acknowledged MUST treat the receipt of a PUSH_PROMISE frame as a + /// connection error (Section 5.4.1) of type PROTOCOL_ERROR. + /// + /// The initial value is 1, which indicates that server push is permitted. + /// Any value other than 0 or 1 MUST be treated as a connection error + /// (Section 5.4.1) of type PROTOCOL_ERROR. + bool enablePush; + + /// Indicates the maximum number of concurrent streams that the sender will + /// allow. This limit is directional: it applies to the number of streams that + /// the sender permits the receiver to create. Initially there is no limit to + /// this value. It is recommended that this value be no smaller than 100, so + /// as to not unnecessarily limit parallelism. + /// + /// A value of 0 for SETTINGS_MAX_CONCURRENT_STREAMS SHOULD NOT be treated as + /// special by endpoints. A zero value does prevent the creation of new + /// streams, however this can also happen for any limit that is exhausted with + /// active streams. Servers SHOULD only set a zero value for short durations; + /// if a server does not wish to accept requests, closing the connection is + /// more appropriate. + int? maxConcurrentStreams; + + /// Indicates the sender's initial window size (in octets) for stream level + /// flow control. The initial value is 2^16-1 (65,535) octets. + /// + /// This setting affects the window size of all streams, including existing + /// streams, see Section 6.9.2. + /// Values above the maximum flow control window size of 231-1 MUST be treated + /// as a connection error (Section 5.4.1) of type FLOW_CONTROL_ERROR. + int initialWindowSize; + + /// Indicates the size of the largest frame payload that the sender is willing + /// to receive, in octets. + /// + /// The initial value is 2^14 (16,384) octets. The value advertised by an + /// endpoint MUST be between this initial value and the maximum allowed frame + /// size (2^24-1 or 16,777,215 octets), inclusive. Values outside this range + /// MUST be treated as a connection error (Section 5.4.1) of type + /// PROTOCOL_ERROR. + int maxFrameSize; + + /// This advisory setting informs a peer of the maximum size of header list + /// that the sender is prepared to accept, in octets. The value is based on + /// the uncompressed size of header fields, including the length of the name + /// and value in octets plus an overhead of 32 octets for each header field. + /// + /// For any given request, a lower limit than what is advertised MAY be + /// enforced. The initial value of this setting is unlimited. + int? maxHeaderListSize; + + ActiveSettings( + {this.headerTableSize = 4096, + this.enablePush = true, + this.maxConcurrentStreams, + this.initialWindowSize = (1 << 16) - 1, + this.maxFrameSize = 1 << 14, + this.maxHeaderListSize}); +} + +/// Handles remote and local connection [Setting]s. +/// +/// Incoming [SettingsFrame]s will be handled here to update the peer settings. +/// Changes to [_toBeAcknowledgedSettings] can be made, the peer will then be +/// notified of the setting changes it should use. +class SettingsHandler extends Object with TerminatableMixin { + /// Certain settings changes can change the maximum allowed dynamic table + /// size used by the HPack encoder. + final HPackEncoder _hpackEncoder; + + final FrameWriter _frameWriter; + + /// A list of outstanding setting changes. + final List<List<Setting>> _toBeAcknowledgedSettings = []; + + /// A list of completers for outstanding setting changes. + final List<Completer> _toBeAcknowledgedCompleters = []; + + /// The local settings, which the remote side ACKed to obey. + final ActiveSettings _acknowledgedSettings; + + /// The peer settings, which we ACKed and are obeying. + final ActiveSettings _peerSettings; + + final _onInitialWindowSizeChangeController = + StreamController<int>.broadcast(sync: true); + + /// Events are fired when a SettingsFrame changes the initial size + /// of stream windows. + Stream<int> get onInitialWindowSizeChange => + _onInitialWindowSizeChangeController.stream; + + SettingsHandler(this._hpackEncoder, this._frameWriter, + this._acknowledgedSettings, this._peerSettings); + + /// The settings for this endpoint of the connection which the remote peer + /// has ACKed and uses. + ActiveSettings get acknowledgedSettings => _acknowledgedSettings; + + /// The settings for the remote endpoint of the connection which this + /// endpoint should use. + ActiveSettings get peerSettings => _peerSettings; + + /// Handles an incoming [SettingsFrame] which can be an ACK or a settings + /// change. + void handleSettingsFrame(SettingsFrame frame) { + ensureNotTerminatedSync(() { + assert(frame.header.streamId == 0); + + if (frame.hasAckFlag) { + assert(frame.header.length == 0); + + if (_toBeAcknowledgedSettings.isEmpty) { + // NOTE: The specification does not say anything about ACKed settings + // which were never sent to the other side. We consider this definitly + // an error. + throw ProtocolException( + 'Received an acknowledged settings frame which did not have a ' + 'outstanding settings request.'); + } + var settingChanges = _toBeAcknowledgedSettings.removeAt(0); + var completer = _toBeAcknowledgedCompleters.removeAt(0); + _modifySettings(_acknowledgedSettings, settingChanges, false); + completer.complete(); + } else { + _modifySettings(_peerSettings, frame.settings, true); + _frameWriter.writeSettingsAckFrame(); + } + }); + } + + @override + void onTerminated(Object? error) { + _toBeAcknowledgedSettings.clear(); + _toBeAcknowledgedCompleters + .forEach((Completer c) => c.completeError(error!)); + } + + Future changeSettings(List<Setting> changes) { + return ensureNotTerminatedAsync(() { + // TODO: Have a timeout: When ACK doesn't get back in a reasonable time + // frame we should quit with ErrorCode.SETTINGS_TIMEOUT. + var completer = Completer(); + _toBeAcknowledgedSettings.add(changes); + _toBeAcknowledgedCompleters.add(completer); + _frameWriter.writeSettingsFrame(changes); + return completer.future; + }); + } + + void _modifySettings( + ActiveSettings base, List<Setting> changes, bool peerSettings) { + for (var setting in changes) { + switch (setting.identifier) { + case Setting.SETTINGS_ENABLE_PUSH: + if (setting.value == 0) { + base.enablePush = false; + } else if (setting.value == 1) { + base.enablePush = true; + } else { + throw ProtocolException( + 'The push setting can be only set to 0 or 1.'); + } + break; + + case Setting.SETTINGS_HEADER_TABLE_SIZE: + base.headerTableSize = setting.value; + if (peerSettings) { + _hpackEncoder.updateMaxSendingHeaderTableSize(base.headerTableSize); + } + break; + + case Setting.SETTINGS_MAX_HEADER_LIST_SIZE: + // TODO: Propagate this signal to the HPackContext. + base.maxHeaderListSize = setting.value; + break; + + case Setting.SETTINGS_MAX_CONCURRENT_STREAMS: + // NOTE: We will not force closing of existing streams if the limit is + // lower than the current number of open streams. But we will prevent + // new streams from being created if the number of existing streams + // is above this limit. + base.maxConcurrentStreams = setting.value; + break; + + case Setting.SETTINGS_INITIAL_WINDOW_SIZE: + if (setting.value < (1 << 31)) { + var difference = setting.value - base.initialWindowSize; + _onInitialWindowSizeChangeController.add(difference); + base.initialWindowSize = setting.value; + } else { + throw FlowControlException('Invalid initial window size.'); + } + break; + + default: + // Spec says to ignore unknown settings. + break; + } + } + } +}
diff --git a/http2/lib/src/streams/stream_handler.dart b/http2/lib/src/streams/stream_handler.dart new file mode 100644 index 0000000..e11a61a --- /dev/null +++ b/http2/lib/src/streams/stream_handler.dart
@@ -0,0 +1,873 @@ +// 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:math'; + +import '../../transport.dart'; + +import '../connection.dart'; +import '../error_handler.dart'; +import '../flowcontrol/connection_queues.dart'; +import '../flowcontrol/queue_messages.dart'; +import '../flowcontrol/stream_queues.dart'; +import '../flowcontrol/window.dart'; +import '../flowcontrol/window_handler.dart'; +import '../frames/frames.dart'; +import '../hpack/hpack.dart'; +import '../settings/settings.dart'; +import '../sync_errors.dart'; + +/// Represents the current state of a stream. +enum StreamState { + ReservedLocal, + ReservedRemote, + Idle, + Open, + HalfClosedLocal, + HalfClosedRemote, + Closed, + + /// The [Terminated] state is an artificial state and signals that this stream + /// has been forcefully terminated. + Terminated, +} + +/// Represents a HTTP/2 stream. +class Http2StreamImpl extends TransportStream + implements ClientTransportStream, ServerTransportStream { + /// The id of this stream. + /// + /// * odd numbered streams are client streams + /// * even numbered streams are opened from the server + @override + final int id; + + // The queue for incoming [StreamMessage]s. + final StreamMessageQueueIn incomingQueue; + + // The queue for outgoing [StreamMessage]s. + final StreamMessageQueueOut outgoingQueue; + + // The stream controller to which the application can + // add outgoing messages. + final StreamController<StreamMessage> _outgoingC; + + final OutgoingStreamWindowHandler windowHandler; + + // The state of this stream. + StreamState state = StreamState.Idle; + + // Error code from RST_STREAM frame, if the stream has been terminated + // remotely. + int? _terminatedErrorCode; + + // Termination handler. Invoked if the stream receives an RST_STREAM frame. + void Function(int)? _onTerminated; + + final ZoneUnaryCallback<bool, Http2StreamImpl> _canPushFun; + final ZoneBinaryCallback<ServerTransportStream, Http2StreamImpl, List<Header>> + _pushStreamFun; + final ZoneUnaryCallback<dynamic, Http2StreamImpl> _terminateStreamFun; + + late StreamSubscription _outgoingCSubscription; + + Http2StreamImpl( + this.incomingQueue, + this.outgoingQueue, + this._outgoingC, + this.id, + this.windowHandler, + this._canPushFun, + this._pushStreamFun, + this._terminateStreamFun); + + /// A stream of data and/or headers from the remote end. + @override + Stream<StreamMessage> get incomingMessages => incomingQueue.messages; + + /// A sink for writing data and/or headers to the remote end. + @override + StreamSink<StreamMessage> get outgoingMessages => _outgoingC.sink; + + /// Streams which the server pushed to this endpoint. + @override + Stream<TransportStreamPush> get peerPushes => incomingQueue.serverPushes; + + @override + bool get canPush => _canPushFun(this); + + /// Pushes a new stream to a client. + /// + /// The [requestHeaders] are the headers to which the pushed stream + /// responds to. + @override + ServerTransportStream push(List<Header> requestHeaders) => + _pushStreamFun(this, requestHeaders); + + @override + void terminate() => _terminateStreamFun(this); + + @override + set onTerminated(void Function(int) handler) { + _onTerminated = handler; + if (_terminatedErrorCode != null && _onTerminated != null) { + _onTerminated!(_terminatedErrorCode!); + } + } + + void _handleTerminated(int errorCode) { + _terminatedErrorCode = errorCode; + if (_onTerminated != null) { + _onTerminated!(_terminatedErrorCode!); + } + } +} + +/// Handles [Frame]s with a non-zero stream-id. +/// +/// It keeps track of open streams, their state, their queues, forwards +/// messages from the connection level to stream level and vise versa. +// TODO: Handle stream/connection queue errors & forward to connection object. +class StreamHandler extends Object with TerminatableMixin, ClosableMixin { + static const int MAX_STREAM_ID = (1 << 31) - 1; + + final FrameWriter _frameWriter; + final ConnectionMessageQueueIn incomingQueue; + final ConnectionMessageQueueOut outgoingQueue; + + final StreamController<TransportStream> _newStreamsC = StreamController(); + + final ActiveSettings _peerSettings; + final ActiveSettings _localSettings; + + final Map<int, Http2StreamImpl> _openStreams = {}; + int nextStreamId; + int lastRemoteStreamId; + + int _highestStreamIdReceived = 0; + + /// Represents the highest stream id this connection has received from the + /// remote side. + int get highestPeerInitiatedStream => _highestStreamIdReceived; + + bool get isServer => nextStreamId.isEven; + + bool get ranOutOfStreamIds => _ranOutOfStreamIds(); + + /// Whether it is possible to open a new stream to the remote end (e.g. based + /// on whether we have reached the limit of maximum concurrent open streams). + bool get canOpenStream => _canCreateNewStream(); + + final ActiveStateHandler _onActiveStateChanged; + + StreamHandler._( + this._frameWriter, + this.incomingQueue, + this.outgoingQueue, + this._peerSettings, + this._localSettings, + this._onActiveStateChanged, + this.nextStreamId, + this.lastRemoteStreamId); + + factory StreamHandler.client( + FrameWriter writer, + ConnectionMessageQueueIn incomingQueue, + ConnectionMessageQueueOut outgoingQueue, + ActiveSettings peerSettings, + ActiveSettings localSettings, + ActiveStateHandler onActiveStateChanged) { + return StreamHandler._(writer, incomingQueue, outgoingQueue, peerSettings, + localSettings, onActiveStateChanged, 1, 0); + } + + factory StreamHandler.server( + FrameWriter writer, + ConnectionMessageQueueIn incomingQueue, + ConnectionMessageQueueOut outgoingQueue, + ActiveSettings peerSettings, + ActiveSettings localSettings, + ActiveStateHandler onActiveStateChanged) { + return StreamHandler._(writer, incomingQueue, outgoingQueue, peerSettings, + localSettings, onActiveStateChanged, 2, -1); + } + + @override + void onTerminated(exception) { + _openStreams.values.toList().forEach((stream) => + _closeStreamAbnormally(stream, exception, propagateException: true)); + startClosing(); + } + + void forceDispatchIncomingMessages() { + _openStreams.forEach((int streamId, Http2StreamImpl stream) { + stream.incomingQueue.forceDispatchIncomingMessages(); + }); + } + + Stream<TransportStream> get incomingStreams => _newStreamsC.stream; + + List<TransportStream> get openStreams => _openStreams.values.toList(); + + void processInitialWindowSizeSettingChange(int difference) { + // If the initialFlowWindow size was changed via a SettingsFrame, all + // existing streams must be updated to reflect this change. + _openStreams.values.forEach((Http2StreamImpl stream) { + stream.windowHandler.processInitialWindowSizeSettingChange(difference); + }); + } + + void processGoawayFrame(GoawayFrame frame) { + var lastStreamId = frame.lastStreamId; + var streamIds = _openStreams.keys + .where((id) => id > lastStreamId && !_isPeerInitiatedStream(id)) + .toList(); + for (var id in streamIds) { + var exception = StreamException( + id, + 'Remote end was telling us to stop. This stream was not processed ' + 'and can therefore be retried (on a new connection).'); + _closeStreamIdAbnormally(id, exception, propagateException: true); + } + } + + //////////////////////////////////////////////////////////////////////////// + //// New local/remote Stream handling + //////////////////////////////////////////////////////////////////////////// + + bool _isPeerInitiatedStream(int streamId) { + var isServerStreamId = streamId.isEven; + var isLocalStream = isServerStreamId == isServer; + return !isLocalStream; + } + + Http2StreamImpl newStream(List<Header> headers, {bool endStream = false}) { + return ensureNotTerminatedSync(() { + var stream = newLocalStream(); + _sendHeaders(stream, headers, endStream: endStream); + return stream; + }); + } + + Http2StreamImpl newLocalStream() { + return ensureNotTerminatedSync(() { + assert(_canCreateNewStream()); + + if (MAX_STREAM_ID < nextStreamId) { + throw StateError( + 'Cannot create new streams, since a wrap around would happen.'); + } + var streamId = nextStreamId; + nextStreamId += 2; + return _newStreamInternal(streamId); + }); + } + + Http2StreamImpl newRemoteStream(int remoteStreamId) { + return ensureNotTerminatedSync(() { + assert(remoteStreamId <= MAX_STREAM_ID); + // NOTE: We cannot enforce that a new stream id is 2 higher than the last + // used stream id. Meaning there can be "holes" in the sense that stream + // ids are not used: + // + // http/2 spec: + // The first use of a new stream identifier implicitly closes all + // streams in the "idle" state that might have been initiated by that + // peer with a lower-valued stream identifier. For example, if a client + // sends a HEADERS frame on stream 7 without ever sending a frame on + // stream 5, then stream 5 transitions to the "closed" state when the + // first frame for stream 7 is sent or received. + + if (remoteStreamId <= lastRemoteStreamId) { + throw ProtocolException('Remote tried to open new stream which is ' + 'not in "idle" state.'); + } + + var sameDirection = (nextStreamId + remoteStreamId) % 2 == 0; + assert(!sameDirection); + + lastRemoteStreamId = remoteStreamId; + return _newStreamInternal(remoteStreamId); + }); + } + + Http2StreamImpl _newStreamInternal(int streamId) { + // For each new stream we must: + // - setup sending/receiving [Window]s with correct initial size + // - setup sending/receiving WindowHandlers which take care of + // updating the windows. + // - setup incoming/outgoing stream queues, which buffer data + // that is not handled by + // * the application [incoming] + // * the underlying transport [outgoing] + // - register incoming stream queue in connection-level queue + + var outgoingStreamWindow = + Window(initialSize: _peerSettings.initialWindowSize); + + var incomingStreamWindow = + Window(initialSize: _localSettings.initialWindowSize); + + var windowOutHandler = OutgoingStreamWindowHandler(outgoingStreamWindow); + + var windowInHandler = IncomingWindowHandler.stream( + _frameWriter, incomingStreamWindow, streamId); + + var streamQueueIn = StreamMessageQueueIn(windowInHandler); + var streamQueueOut = + StreamMessageQueueOut(streamId, windowOutHandler, outgoingQueue); + + incomingQueue.insertNewStreamMessageQueue(streamId, streamQueueIn); + + var _outgoingC = StreamController<StreamMessage>(); + var stream = Http2StreamImpl(streamQueueIn, streamQueueOut, _outgoingC, + streamId, windowOutHandler, _canPush, _push, _terminateStream); + final wasIdle = _openStreams.isEmpty; + _openStreams[stream.id] = stream; + + _setupOutgoingMessageHandling(stream); + + // Handle incoming stream cancellation. RST is only sent when streamQueueOut + // has been closed because RST make the stream 'closed'. + streamQueueIn.onCancel.then((_) { + // If our side is done sending data, i.e. we have enqueued the + // end-of-stream in the outgoing message queue, but the remote end is + // still sending us data, despite us not being interested in it, we will + // reset the stream. + if (stream.state == StreamState.HalfClosedLocal) { + stream.outgoingQueue + .enqueueMessage(ResetStreamMessage(stream.id, ErrorCode.CANCEL)); + } + }); + + // NOTE: We are not interested whether the streams were normally finished + // or abnormally terminated. Therefore we use 'catchError((_) {})'! + var streamDone = [streamQueueIn.done, streamQueueOut.done]; + Future.wait(streamDone).catchError((_) {}).whenComplete(() { + _cleanupClosedStream(stream); + }); + + if (wasIdle) { + _onActiveStateChanged(true); + } + + return stream; + } + + bool _canPush(Http2StreamImpl stream) { + var openState = stream.state == StreamState.Open || + stream.state == StreamState.HalfClosedRemote; + var pushEnabled = _peerSettings.enablePush; + return openState && + pushEnabled && + _canCreateNewStream() && + !_ranOutOfStreamIds(); + } + + ServerTransportStream _push( + Http2StreamImpl stream, List<Header> requestHeaders) { + if (stream.state != StreamState.Open && + stream.state != StreamState.HalfClosedRemote) { + throw StateError('Cannot push based on a stream that is neither open ' + 'nor half-closed-remote.'); + } + + if (!_peerSettings.enablePush) { + throw StateError('Client did disable server pushes.'); + } + + if (!_canCreateNewStream()) { + throw StateError('Maximum number of streams reached.'); + } + + if (_ranOutOfStreamIds()) { + throw StateError('There are no more stream ids left. Please use a ' + 'new connection.'); + } + + var pushStream = newLocalStream(); + + // NOTE: Since there was no real request from the client, we simulate it + // by adding a synthetic `endStream = true` Data message into the incoming + // queue. + _changeState(pushStream, StreamState.ReservedLocal); + // TODO: We should wait for us to send the headers frame before doing this + // transition. + _changeState(pushStream, StreamState.HalfClosedRemote); + pushStream.incomingQueue + .enqueueMessage(DataMessage(stream.id, const <int>[], true)); + + _frameWriter.writePushPromiseFrame( + stream.id, pushStream.id, requestHeaders); + + return pushStream; + } + + void _terminateStream(Http2StreamImpl stream) { + if (stream.state == StreamState.Open || + stream.state == StreamState.HalfClosedLocal || + stream.state == StreamState.HalfClosedRemote || + stream.state == StreamState.ReservedLocal || + stream.state == StreamState.ReservedRemote) { + _frameWriter.writeRstStreamFrame(stream.id, ErrorCode.CANCEL); + _closeStreamAbnormally(stream, null, propagateException: false); + } + } + + void _setupOutgoingMessageHandling(Http2StreamImpl stream) { + stream._outgoingCSubscription = + stream._outgoingC.stream.listen((StreamMessage msg) { + if (!wasTerminated) { + _handleNewOutgoingMessage(stream, msg); + } + }, onError: (error, stack) { + if (!wasTerminated) { + stream.terminate(); + } + }, onDone: () { + if (!wasTerminated) { + // Stream should already have been closed by the last frame, but we + // allow multiple close calls, just to make sure. + _handleOutgoingClose(stream); + } + }); + stream.outgoingQueue.bufferIndicator.bufferEmptyEvents.listen((_) { + if (stream._outgoingCSubscription.isPaused) { + stream._outgoingCSubscription.resume(); + } + }); + } + + void _handleNewOutgoingMessage(Http2StreamImpl stream, StreamMessage msg) { + if (stream.state == StreamState.Idle) { + if (msg is! HeadersStreamMessage) { + var exception = TransportException( + 'The first message on a stream needs to be a headers frame.'); + _closeStreamAbnormally(stream, exception); + return; + } + _changeState(stream, StreamState.Open); + } + + if (msg is DataStreamMessage) { + _sendData(stream, msg.bytes, endStream: msg.endStream); + } else if (msg is HeadersStreamMessage) { + _sendHeaders(stream, msg.headers, endStream: msg.endStream); + } + + if (stream.outgoingQueue.bufferIndicator.wouldBuffer && + !stream._outgoingCSubscription.isPaused) { + stream._outgoingCSubscription.pause(); + } + } + + void _handleOutgoingClose(Http2StreamImpl stream) { + // We allow multiple close calls. + if (stream.state != StreamState.HalfClosedLocal && + stream.state != StreamState.Closed && + stream.state != StreamState.Terminated) { + _sendData(stream, const [], endStream: true); + } + } + + //////////////////////////////////////////////////////////////////////////// + //// Process incoming stream frames + //////////////////////////////////////////////////////////////////////////// + + void processStreamFrame(ConnectionState connectionState, Frame frame) { + try { + _processStreamFrameInternal(connectionState, frame); + } on StreamClosedException catch (exception) { + _frameWriter.writeRstStreamFrame( + exception.streamId, ErrorCode.STREAM_CLOSED); + _closeStreamIdAbnormally(exception.streamId, exception); + } on StreamException catch (exception) { + _frameWriter.writeRstStreamFrame( + exception.streamId, ErrorCode.INTERNAL_ERROR); + _closeStreamIdAbnormally(exception.streamId, exception); + } + } + + void _processStreamFrameInternal( + ConnectionState connectionState, Frame frame) { + // If we initiated a close of the connection and the received frame belongs + // to a stream id which is higher than the last peer-initiated stream we + // processed, we'll ignore it. + // http/2 spec: + // After sending a GOAWAY frame, the sender can discard frames for + // streams initiated by the receiver with identifiers higher than the + // identified last stream. However, any frames that alter connection + // state cannot be completely ignored. For instance, HEADERS, + // PUSH_PROMISE, and CONTINUATION frames MUST be minimally processed to + // ensure the state maintained for header compression is consistent + // (see Section 4.3); similarly, DATA frames MUST be counted toward + // the connection flow-control window. Failure to process these + // frames can cause flow control or header compression state to become + // unsynchronized. + if (connectionState.activeFinishing && + _isPeerInitiatedStream(frame.header.streamId) && + frame.header.streamId > highestPeerInitiatedStream) { + // Even if the frame will be ignored, we still need to process it in a + // minimal way to ensure the connection window will be updated. + if (frame is DataFrame) { + incomingQueue.processIgnoredDataFrame(frame); + } + return null; + } + + // TODO: Consider splitting this method into client/server handling. + return ensureNotTerminatedSync(() { + var stream = _openStreams[frame.header.streamId]; + if (stream == null) { + bool frameBelongsToIdleStream() { + var streamId = frame.header.streamId; + var isServerStreamId = frame.header.streamId.isEven; + var isLocalStream = isServerStreamId == isServer; + var isIdleStream = isLocalStream + ? streamId >= nextStreamId + : streamId > lastRemoteStreamId; + return isIdleStream; + } + + if (_isPeerInitiatedStream(frame.header.streamId)) { + // Update highest stream id we received and processed (we update it + // before processing, so if it was an error, the client will not + // retry it). + _highestStreamIdReceived = + max(_highestStreamIdReceived, frame.header.streamId); + } + + if (frame is HeadersFrame) { + if (isServer) { + var newStream = newRemoteStream(frame.header.streamId); + _changeState(newStream, StreamState.Open); + + _handleHeadersFrame(newStream, frame); + _newStreamsC.add(newStream); + } else { + // A server cannot open new streams to the client. The only way + // for a server to start a new stream is via a PUSH_PROMISE_FRAME. + throw ProtocolException( + 'HTTP/2 clients cannot receive HEADER_FRAMEs as a connection' + 'attempt.'); + } + } else if (frame is WindowUpdateFrame) { + if (frameBelongsToIdleStream()) { + // We treat this as a protocol error even though not enforced + // or specified by the HTTP/2 spec. + throw ProtocolException( + 'Got a WINDOW_UPDATE_FRAME for an "idle" stream id.'); + } else { + // We must be able to receive window update frames for streams that + // have been already closed. The specification does not mention + // what happens if the streamId is belonging to an "idle" / unused + // stream. + } + } else if (frame is RstStreamFrame) { + if (frameBelongsToIdleStream()) { + // [RstFrame]s for streams which haven't been established (known as + // idle streams) must be treated as a connection error. + throw ProtocolException( + 'Got a RST_STREAM_FRAME for an "idle" stream id.'); + } else { + // [RstFrame]s for already dead (known as "closed") streams should + // be ignored. (If the stream was in "HalfClosedRemote" and we did + // send an endStream=true, it will be removed from the stream set). + } + } else if (frame is PriorityFrame) { + // http/2 spec: + // The PRIORITY frame can be sent for a stream in the "idle" or + // "closed" states. This allows for the reprioritization of a + // group of dependent streams by altering the priority of an + // unused or closed parent stream. + // + // As long as we do not handle stream priorities, we can safely ignore + // such frames on idle streams. + // + // NOTE: Firefox for example sends [PriorityFrame]s even without + // opening any streams (e.g. streams 3,5,7,9,11 [PriorityFrame]s and + // stream 13 is the first real stream opened by a [HeadersFrame]. + // + // TODO: When implementing priorities for HTTP/2 streams, these frames + // need to be taken into account. + } else if (frame is PushPromiseFrame) { + throw ProtocolException('Cannot push on a non-existent stream ' + '(stream ${frame.header.streamId} does not exist)'); + } else { + throw StreamClosedException( + frame.header.streamId, + 'No open stream found and was not a headers frame opening a ' + 'new stream.'); + } + } else { + if (frame is HeadersFrame) { + _handleHeadersFrame(stream, frame); + } else if (frame is DataFrame) { + _handleDataFrame(stream, frame); + } else if (frame is PushPromiseFrame) { + _handlePushPromiseFrame(stream, frame); + } else if (frame is WindowUpdateFrame) { + _handleWindowUpdate(stream, frame); + } else if (frame is RstStreamFrame) { + _handleRstFrame(stream, frame); + } else { + throw ProtocolException( + 'Unsupported frame type ${frame.runtimeType}.'); + } + } + }); + } + + void _handleHeadersFrame(Http2StreamImpl stream, HeadersFrame frame) { + if (stream.state == StreamState.ReservedRemote) { + _changeState(stream, StreamState.HalfClosedLocal); + } + + if (stream.state != StreamState.Open && + stream.state != StreamState.HalfClosedLocal) { + throw StreamClosedException( + stream.id, 'Expected open state (was: ${stream.state}).'); + } + + incomingQueue.processHeadersFrame(frame); + + if (frame.hasEndStreamFlag) _handleEndOfStreamRemote(stream); + } + + void _handleDataFrame(Http2StreamImpl stream, DataFrame frame) { + if (stream.state != StreamState.Open && + stream.state != StreamState.HalfClosedLocal) { + throw StreamClosedException( + stream.id, 'Expected open state (was: ${stream.state}).'); + } + + incomingQueue.processDataFrame(frame); + + if (frame.hasEndStreamFlag) _handleEndOfStreamRemote(stream); + } + + void _handlePushPromiseFrame(Http2StreamImpl stream, PushPromiseFrame frame) { + if (stream.state != StreamState.Open && + stream.state != StreamState.HalfClosedLocal) { + throw ProtocolException('Expected open state (was: ${stream.state}).'); + } + + var pushedStream = newRemoteStream(frame.promisedStreamId); + _changeState(pushedStream, StreamState.ReservedRemote); + + incomingQueue.processPushPromiseFrame(frame, pushedStream); + } + + void _handleWindowUpdate(Http2StreamImpl stream, WindowUpdateFrame frame) { + stream.windowHandler.processWindowUpdate(frame); + } + + void _handleRstFrame(Http2StreamImpl stream, RstStreamFrame frame) { + stream._handleTerminated(frame.errorCode); + var exception = StreamTransportException( + 'Stream was terminated by peer (errorCode: ${frame.errorCode}).'); + _closeStreamAbnormally(stream, exception, propagateException: true); + } + + void _handleEndOfStreamRemote(Http2StreamImpl stream) { + if (stream.state == StreamState.Open) { + _changeState(stream, StreamState.HalfClosedRemote); + } else if (stream.state == StreamState.HalfClosedLocal) { + _changeState(stream, StreamState.Closed); + // TODO: We have to make sure that we + // - remove the stream for data structures which only care about the + // state + // - keep the stream in data structures which need to be emptied + // (e.g. MessageQueues which are not empty yet). + _openStreams.remove(stream.id); + } else { + throw StateError( + 'Got an end-of-stream from the remote end, but this stream is ' + 'neither in the Open nor in the HalfClosedLocal state. ' + 'This should never happen.'); + } + } + + //////////////////////////////////////////////////////////////////////////// + //// Process outgoing stream messages + //////////////////////////////////////////////////////////////////////////// + + void _sendHeaders(Http2StreamImpl stream, List<Header> headers, + {bool endStream = false}) { + if (stream.state != StreamState.Idle && + stream.state != StreamState.Open && + stream.state != StreamState.HalfClosedRemote) { + throw StateError('Idle state expected.'); + } + + stream.outgoingQueue + .enqueueMessage(HeadersMessage(stream.id, headers, endStream)); + + if (stream.state == StreamState.Idle) { + _changeState(stream, StreamState.Open); + } + + if (endStream) { + _endStream(stream); + } + } + + void _sendData(Http2StreamImpl stream, List<int> data, + {bool endStream = false}) { + if (stream.state != StreamState.Open && + stream.state != StreamState.HalfClosedRemote) { + throw StateError('Open state expected (was: ${stream.state}).'); + } + + stream.outgoingQueue + .enqueueMessage(DataMessage(stream.id, data, endStream)); + + if (endStream) { + _endStream(stream); + } + } + + void _endStream(Http2StreamImpl stream) { + if (stream.state == StreamState.Open) { + _changeState(stream, StreamState.HalfClosedLocal); + } else if (stream.state == StreamState.HalfClosedRemote) { + _changeState(stream, StreamState.Closed); + } else { + throw StateError('Invalid state transition. This should never happen.'); + } + } + + //////////////////////////////////////////////////////////////////////////// + //// Stream closing + //////////////////////////////////////////////////////////////////////////// + + void _cleanupClosedStream(Http2StreamImpl stream) { + // NOTE: This function should only be called once + // * all incoming data has been delivered to the application + // * all outgoing data has been added to the connection queue. + incomingQueue.removeStreamMessageQueue(stream.id); + _openStreams.remove(stream.id); + if (stream.state != StreamState.Terminated) { + _changeState(stream, StreamState.Terminated); + } + if (_openStreams.isEmpty) { + _onActiveStateChanged(false); + } + onCheckForClose(); + } + + void _closeStreamIdAbnormally(int streamId, Exception exception, + {bool propagateException = false}) { + var stream = _openStreams[streamId]; + if (stream != null) { + _closeStreamAbnormally(stream, exception, + propagateException: propagateException); + } + } + + void _closeStreamAbnormally(Http2StreamImpl stream, Object? exception, + {bool propagateException = false}) { + incomingQueue.removeStreamMessageQueue(stream.id); + + if (stream.state != StreamState.Terminated) { + _changeState(stream, StreamState.Terminated); + } + stream.incomingQueue.terminate(propagateException ? exception : null); + stream._outgoingCSubscription.cancel(); + stream._outgoingC.close(); + + // NOTE: we're not adding an error here. + stream.outgoingQueue.terminate(); + + onCheckForClose(); + } + + @override + void onClosing() { + _newStreamsC.close(); + } + + @override + void onCheckForClose() { + if (isClosing && _openStreams.isEmpty) { + closeWithValue(); + } + } + + //////////////////////////////////////////////////////////////////////////// + //// State transitioning & Counting of active streams + //////////////////////////////////////////////////////////////////////////// + + /// The number of streams which we initiated and which are in one of the open + /// states (i.e. [StreamState.Open], [StreamState.HalfClosedLocal] or + /// [StreamState.HalfClosedRemote]) + int _numberOfActiveStreams = 0; + + bool _canCreateNewStream() { + var limit = _peerSettings.maxConcurrentStreams; + return limit == null || _numberOfActiveStreams < limit; + } + + bool _ranOutOfStreamIds() { + return nextStreamId > MAX_STREAM_ID; + } + + void _changeState(Http2StreamImpl stream, StreamState to) { + var from = stream.state; + + // In checked mode we'll test that the state transition is allowed. + assert((from == StreamState.Idle && to == StreamState.ReservedLocal) || + (from == StreamState.Idle && to == StreamState.ReservedRemote) || + (from == StreamState.Idle && to == StreamState.Open) || + (from == StreamState.Open && to == StreamState.HalfClosedLocal) || + (from == StreamState.Open && to == StreamState.HalfClosedRemote) || + (from == StreamState.Open && to == StreamState.Closed) || + (from == StreamState.HalfClosedLocal && to == StreamState.Closed) || + (from == StreamState.HalfClosedRemote && to == StreamState.Closed) || + (from == StreamState.ReservedLocal && + to == StreamState.HalfClosedRemote) || + (from == StreamState.ReservedLocal && to == StreamState.Closed) || + (from == StreamState.ReservedRemote && to == StreamState.Closed) || + (from == StreamState.ReservedRemote && + to == StreamState.HalfClosedLocal) || + (from != StreamState.Terminated && to == StreamState.Terminated)); + + // If we initiated the stream and it became "open" or "closed" we need to + // update the [_numberOfActiveStreams] counter. + if (_didInitiateStream(stream)) { + // NOTE: We wait until the stream is completely done. + // (If we waited only until `StreamState.Closed` then we might still have + // the endStream header/data message buffered, but not yet sent out). + switch (stream.state) { + case StreamState.ReservedLocal: + case StreamState.ReservedRemote: + case StreamState.Idle: + if (to == StreamState.Open || + to == StreamState.HalfClosedLocal || + to == StreamState.HalfClosedRemote) { + _numberOfActiveStreams++; + } + break; + case StreamState.Open: + case StreamState.HalfClosedLocal: + case StreamState.HalfClosedRemote: + case StreamState.Closed: + if (to == StreamState.Terminated) { + _numberOfActiveStreams--; + } + break; + case StreamState.Terminated: + // There is nothing to do here. + break; + } + } + stream.state = to; + } + + bool _didInitiateStream(Http2StreamImpl stream) { + var id = stream.id; + return (isServer && id.isEven) || (!isServer && id.isOdd); + } +}
diff --git a/http2/lib/src/sync_errors.dart b/http2/lib/src/sync_errors.dart new file mode 100644 index 0000000..2423e6a --- /dev/null +++ b/http2/lib/src/sync_errors.dart
@@ -0,0 +1,53 @@ +// 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. + +class ProtocolException implements Exception { + final String _message; + + ProtocolException(this._message); + + @override + String toString() => 'ProtocolError: $_message'; +} + +class FlowControlException implements Exception { + final String _message; + + FlowControlException(this._message); + + @override + String toString() => 'FlowControlException: $_message'; +} + +class FrameSizeException implements Exception { + final String _message; + + FrameSizeException(this._message); + + @override + String toString() => 'FrameSizeException: $_message'; +} + +class TerminatedException implements Exception { + @override + String toString() => 'TerminatedException: The object has been terminated.'; +} + +class StreamException implements Exception { + final String _message; + final int streamId; + + StreamException(this.streamId, this._message); + + @override + String toString() => 'StreamException(stream id: $streamId): $_message'; +} + +class StreamClosedException extends StreamException { + StreamClosedException(int streamId, [String message = '']) + : super(streamId, message); + + @override + String toString() => 'StreamClosedException(stream id: $streamId): $_message'; +}
diff --git a/http2/lib/src/testing/client.dart b/http2/lib/src/testing/client.dart new file mode 100644 index 0000000..ecf4e99 --- /dev/null +++ b/http2/lib/src/testing/client.dart
@@ -0,0 +1,144 @@ +// 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:convert' show ascii; +import 'dart:io'; + +import '../../transport.dart'; + +class Request { + final String method; + final Uri uri; + + Request(this.method, this.uri); +} + +class Response { + final Map<String, List<String>> headers; + final Stream<List<int>> stream; + final Stream<ServerPush> serverPushes; + + Response(this.headers, this.stream, this.serverPushes); +} + +class ServerPush { + final Map<String, List<String>> requestHeaders; + final Future<Response> response; + + ServerPush(this.requestHeaders, this.response); +} + +class ClientConnection { + final ClientTransportConnection connection; + + /// Assumes the protocol on [socket] was negogiated to be http/2. + /// + /// If [settings] are omitted, the default [ClientSettings] will be used. + ClientConnection(Socket socket, {ClientSettings? settings}) + : connection = + ClientTransportConnection.viaSocket(socket, settings: settings); + + Future<Response> makeRequest(Request request) { + var path = request.uri.path; + if (path.isEmpty) path = '/'; + + var headers = [ + Header.ascii(':method', request.method), + Header.ascii(':path', path), + Header.ascii(':scheme', request.uri.scheme), + Header.ascii(':authority', '${request.uri.host}'), + ]; + + return _handleStream(connection.makeRequest(headers, endStream: true)); + } + + Future close() { + return connection.finish(); + } + + Future<Response> _handleStream(ClientTransportStream stream) { + var completer = Completer<Response>(); + var isFirst = true; + var controller = StreamController<List<int>>(); + var serverPushController = StreamController<ServerPush>(sync: true); + stream.incomingMessages.listen((StreamMessage msg) { + if (isFirst) { + isFirst = false; + var headerMap = _convertHeaders((msg as HeadersStreamMessage).headers); + completer.complete(Response( + headerMap, controller.stream, serverPushController.stream)); + } else { + controller.add((msg as DataStreamMessage).bytes); + } + }, onDone: controller.close); + _handlePeerPushes(stream.peerPushes).pipe(serverPushController); + return completer.future; + } + + Stream<ServerPush> _handlePeerPushes( + Stream<TransportStreamPush> serverPushes) { + var pushesController = StreamController<ServerPush>(); + serverPushes.listen((TransportStreamPush push) { + var responseCompleter = Completer<Response>(); + var serverPush = ServerPush( + _convertHeaders(push.requestHeaders), responseCompleter.future); + + pushesController.add(serverPush); + + var isFirst = true; + var dataController = StreamController<List<int>>(); + push.stream.incomingMessages.listen((StreamMessage msg) { + if (isFirst) { + isFirst = false; + var headerMap = + _convertHeaders((msg as HeadersStreamMessage).headers); + var response = Response( + headerMap, dataController.stream, Stream.fromIterable([])); + responseCompleter.complete(response); + } else { + dataController.add((msg as DataStreamMessage).bytes); + } + }, onDone: dataController.close); + }, onDone: pushesController.close); + return pushesController.stream; + } + + Map<String, List<String>> _convertHeaders(List<Header> headers) { + var headerMap = <String, List<String>>{}; + for (var header in headers) { + headerMap + .putIfAbsent(ascii.decode(header.name), () => []) + .add(ascii.decode(header.value)); + } + return headerMap; + } +} + +/// Tries to connect to [uri] via a secure socket connection and establishes a +/// http/2 connection. +/// +/// If [allowServerPushes] is `true`, server pushes need to be handled by the +/// client. The maximum number of concurrent server pushes can be configured via +/// [maxConcurrentPushes] (default is `null` meaning no limit). +Future<ClientConnection> connect(Uri uri, + {bool allowServerPushes = false, int? maxConcurrentPushes}) async { + const Http2AlpnProtocols = <String>['h2-14', 'h2-15', 'h2-16', 'h2-17', 'h2']; + + var useSSL = uri.scheme == 'https'; + var settings = ClientSettings( + concurrentStreamLimit: maxConcurrentPushes, + allowServerPushes: allowServerPushes); + if (useSSL) { + var socket = await SecureSocket.connect(uri.host, uri.port, + supportedProtocols: Http2AlpnProtocols); + if (!Http2AlpnProtocols.contains(socket.selectedProtocol)) { + throw Exception('Server does not support HTTP/2.'); + } + return ClientConnection(socket, settings: settings); + } else { + var socket = await Socket.connect(uri.host, uri.port); + return ClientConnection(socket, settings: settings); + } +}
diff --git a/http2/lib/src/testing/debug.dart b/http2/lib/src/testing/debug.dart new file mode 100644 index 0000000..243e60f --- /dev/null +++ b/http2/lib/src/testing/debug.dart
@@ -0,0 +1,137 @@ +// 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:convert'; +import 'dart:io'; + +import '../../transport.dart'; +import '../connection_preface.dart'; +import '../frames/frames.dart'; +import '../settings/settings.dart'; + +final jsonEncoder = JsonEncoder.withIndent(' '); + +TransportConnection debugPrintingConnection(Socket socket, + {bool isServer = true, bool verbose = true}) { + TransportConnection connection; + + var incoming = decodeVerbose(socket, isServer, verbose: verbose); + var outgoing = decodeOutgoingVerbose(socket, isServer, verbose: verbose); + if (isServer) { + connection = ServerTransportConnection.viaStreams(incoming, outgoing); + } else { + connection = ClientTransportConnection.viaStreams(incoming, outgoing); + } + return connection; +} + +Stream<List<int>> decodeVerbose(Stream<List<int>> inc, bool isServer, + {bool verbose = true}) { + var name = isServer ? 'server' : 'client'; + + var sc = StreamController<List<int>>(); + var sDebug = StreamController<List<int>>(); + + _pipeAndCopy(inc, sc, sDebug); + + if (!isServer) { + _decodeFrames(sDebug.stream).listen((frame) { + print('[$name/stream:${frame.header.streamId}] ' + 'Incoming ${frame.runtimeType}:'); + if (verbose) { + print(jsonEncoder.convert(frame.toJson())); + print(''); + } + }, onError: (e, s) { + print('[$name] Stream error: $e.'); + }, onDone: () { + print('[$name] Closed.'); + }); + } else { + var s3 = readConnectionPreface(sDebug.stream); + _decodeFrames(s3).listen((frame) { + print('[$name/stream:${frame.header.streamId}] ' + 'Incoming ${frame.runtimeType}:'); + if (verbose) { + print(jsonEncoder.convert(frame.toJson())); + print(''); + } + }, onError: (e, s) { + print('[$name] Stream error: $e.'); + }, onDone: () { + print('[$name] Closed.'); + }); + } + + return sc.stream; +} + +StreamSink<List<int>> decodeOutgoingVerbose( + StreamSink<List<int>> sink, bool isServer, + {bool verbose = true}) { + var name = isServer ? 'server' : 'client'; + + var proxySink = StreamController<List<int>>(); + var copy = StreamController<List<int>>(); + + if (!isServer) { + _decodeFrames(readConnectionPreface(copy.stream)).listen((Frame frame) { + print('[$name/stream:${frame.header.streamId}] ' + 'Outgoing ${frame.runtimeType}:'); + if (verbose) { + print(jsonEncoder.convert(frame.toJson())); + print(''); + } + }, onError: (e, s) { + print('[$name] Outgoing stream error: $e'); + }, onDone: () { + print('[$name] Closing.'); + }); + } else { + _decodeFrames(copy.stream).listen((Frame frame) { + print('[$name/stream:${frame.header.streamId}] ' + 'Outgoing ${frame.runtimeType}:'); + if (verbose) { + print(jsonEncoder.convert(frame.toJson())); + print(''); + } + }, onError: (e, s) { + print('[$name] Outgoing stream error: $e'); + }, onDone: () { + print('[$name] Closing.'); + proxySink.close(); + }); + } + + _pipeAndCopy(proxySink.stream, sink, copy); + + return proxySink; +} + +Stream<Frame> _decodeFrames(Stream<List<int>> bytes) { + var settings = ActiveSettings(); + var decoder = FrameReader(bytes, settings); + return decoder.startDecoding(); +} + +Future _pipeAndCopy(Stream<List<int>> from, StreamSink to, StreamSink to2) { + var c = Completer(); + from.listen((List<int> data) { + to.add(data); + to2.add(data); + }, onError: (Object e, StackTrace s) { + to.addError(e, s); + to2.addError(e, s); + }, onDone: () { + Future.wait([to.close(), to2.close()]) + .then(c.complete) + .catchError(c.completeError); + }); + return c.future; +} + +void print(String s) { + stderr.writeln(s); +}
diff --git a/http2/lib/transport.dart b/http2/lib/transport.dart new file mode 100644 index 0000000..f43353d --- /dev/null +++ b/http2/lib/transport.dart
@@ -0,0 +1,243 @@ +// 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 'src/connection.dart'; +import 'src/hpack/hpack.dart' show Header; + +export 'src/hpack/hpack.dart' show Header; + +typedef ActiveStateHandler = void Function(bool isActive); + +/// Settings for a [TransportConnection]. +abstract class Settings { + /// The maximum number of concurrent streams the remote end can open + /// (defaults to being unlimited). + final int? concurrentStreamLimit; + + /// The default stream window size the remote peer can use when creating new + /// streams (defaults to 65535 bytes). + final int? streamWindowSize; + + const Settings({this.concurrentStreamLimit, this.streamWindowSize}); +} + +/// Settings for a [TransportConnection] a server can make. +class ServerSettings extends Settings { + const ServerSettings({int? concurrentStreamLimit, int? streamWindowSize}) + : super( + concurrentStreamLimit: concurrentStreamLimit, + streamWindowSize: streamWindowSize); +} + +/// Settings for a [TransportConnection] a client can make. +class ClientSettings extends Settings { + /// Whether the client allows pushes from the server (defaults to false). + final bool allowServerPushes; + + const ClientSettings( + {int? concurrentStreamLimit, + int? streamWindowSize, + this.allowServerPushes = false}) + : super( + concurrentStreamLimit: concurrentStreamLimit, + streamWindowSize: streamWindowSize); +} + +/// Represents a HTTP/2 connection. +abstract class TransportConnection { + /// Pings the other end. + Future ping(); + + /// Sets the active state callback. + /// + /// This callback is invoked with `true` when the number of active streams + /// goes from 0 to 1 (the connection goes from idle to active), and with + /// `false` when the number of active streams becomes 0 (the connection goes + /// from active to idle). + set onActiveStateChanged(ActiveStateHandler callback); + + /// Future which completes when the first SETTINGS frame is received from + /// the peer. + Future<void> get onInitialPeerSettingsReceived; + + /// Finish this connection. + /// + /// No new streams will be accepted or can be created. + Future finish(); + + /// Terminates this connection forcefully. + Future terminate(); +} + +abstract class ClientTransportConnection extends TransportConnection { + factory ClientTransportConnection.viaSocket(Socket socket, + {ClientSettings? settings}) => + ClientTransportConnection.viaStreams(socket, socket, settings: settings); + + factory ClientTransportConnection.viaStreams( + Stream<List<int>> incoming, StreamSink<List<int>> outgoing, + {ClientSettings? settings}) { + settings ??= const ClientSettings(); + return ClientConnection(incoming, outgoing, settings); + } + + /// Whether this connection is open and can be used to make new requests + /// via [makeRequest]. + bool get isOpen; + + /// Creates a new outgoing stream. + ClientTransportStream makeRequest(List<Header> headers, + {bool endStream = false}); +} + +abstract class ServerTransportConnection extends TransportConnection { + factory ServerTransportConnection.viaSocket(Socket socket, + {ServerSettings? settings}) { + return ServerTransportConnection.viaStreams(socket, socket, + settings: settings); + } + + factory ServerTransportConnection.viaStreams( + Stream<List<int>> incoming, StreamSink<List<int>> outgoing, + {ServerSettings? settings = + const ServerSettings(concurrentStreamLimit: 1000)}) { + settings ??= const ServerSettings(); + return ServerConnection(incoming, outgoing, settings); + } + + /// Incoming HTTP/2 streams. + Stream<ServerTransportStream> get incomingStreams; +} + +/// Represents a HTTP/2 stream. +abstract class TransportStream { + /// The id of this stream. + /// + /// * odd numbered streams are client streams + /// * even numbered streams are opened from the server + int get id; + + /// A stream of data and/or headers from the remote end. + Stream<StreamMessage> get incomingMessages; + + /// A sink for writing data and/or headers to the remote end. + StreamSink<StreamMessage> get outgoingMessages; + + /// Sets the termination handler on this stream. + /// + /// The handler will be called if the stream receives an RST_STREAM frame. + set onTerminated(void Function(int?) value); + + /// Terminates this HTTP/2 stream in an un-normal way. + /// + /// For normal termination, one can cancel the [StreamSubscription] from + /// `incoming.listen()` and close the `outgoing` [StreamSink]. + /// + /// Terminating this HTTP/2 stream will free up all resources associated with + /// it locally and will notify the remote end that this stream is no longer + /// used. + void terminate(); + + // For convenience only. + void sendHeaders(List<Header> headers, {bool endStream = false}) { + outgoingMessages.add(HeadersStreamMessage(headers, endStream: endStream)); + if (endStream) outgoingMessages.close(); + } + + void sendData(List<int> bytes, {bool endStream = false}) { + outgoingMessages.add(DataStreamMessage(bytes, endStream: endStream)); + if (endStream) outgoingMessages.close(); + } +} + +abstract class ClientTransportStream extends TransportStream { + /// Streams which the remote end pushed to this endpoint. + /// + /// If peer pushes were enabled, the client is responsible to either + /// handle or reject any peer push. + Stream<TransportStreamPush> get peerPushes; +} + +abstract class ServerTransportStream extends TransportStream { + /// Whether a method to [push] will succeed. Requirements for this getter to + /// return `true` are: + /// * this stream must be in the Open or HalfClosedRemote state + /// * the client needs to have the "enable push" settings enabled + /// * the number of active streams has not reached the maximum + bool get canPush; + + /// Pushes a new stream to the remote peer. + ServerTransportStream push(List<Header> requestHeaders); +} + +/// Represents a message which can be sent over a HTTP/2 stream. +abstract class StreamMessage { + final bool endStream; + + StreamMessage({bool? endStream}) : endStream = endStream ?? false; +} + +/// Represents a data message which can be sent over a HTTP/2 stream. +class DataStreamMessage extends StreamMessage { + final List<int> bytes; + + DataStreamMessage(this.bytes, {bool? endStream}) + : super(endStream: endStream); + + @override + String toString() => 'DataStreamMessage(${bytes.length} bytes)'; +} + +/// Represents a headers message which can be sent over a HTTP/2 stream. +class HeadersStreamMessage extends StreamMessage { + final List<Header> headers; + + HeadersStreamMessage(this.headers, {bool? endStream}) + : super(endStream: endStream); + + @override + String toString() => 'HeadersStreamMessage(${headers.length} headers)'; +} + +/// Represents a remote stream push. +class TransportStreamPush { + /// The request headers which [stream] is the response to. + final List<Header> requestHeaders; + + /// The remote stream push. + final ClientTransportStream stream; + + TransportStreamPush(this.requestHeaders, this.stream); + + @override + String toString() => + 'TransportStreamPush(${requestHeaders.length} request headers headers)'; +} + +/// An exception thrown by the HTTP/2 implementation. +class TransportException implements Exception { + final String message; + + TransportException(this.message); + + @override + String toString() => 'HTTP/2 error: $message'; +} + +/// An exception thrown when a HTTP/2 connection error occurred. +class TransportConnectionException extends TransportException { + final int errorCode; + + TransportConnectionException(int errorCode, String details) + : errorCode = errorCode, + super('Connection error: $details (errorCode: $errorCode)'); +} + +/// An exception thrown when a HTTP/2 stream error occured. +class StreamTransportException extends TransportException { + StreamTransportException(String details) : super('Stream error: $details'); +}
diff --git a/http2/manual_test/out_of_stream_ids_test.dart b/http2/manual_test/out_of_stream_ids_test.dart new file mode 100644 index 0000000..a3d9e6b --- /dev/null +++ b/http2/manual_test/out_of_stream_ids_test.dart
@@ -0,0 +1,61 @@ +// 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. + +/// --------------------------------------------------------------------------- +/// In order to run this test one needs to change the following line in +/// ../lib/src/streams/stream_handler.dart +/// +/// - static const int MAX_STREAM_ID = (1 << 31) - 1; +/// + static const int MAX_STREAM_ID = (1 << 5) - 1; +/// +/// without this patch this test will run for a _long_ time. +/// --------------------------------------------------------------------------- + +import 'dart:async'; + +import 'package:test/test.dart'; +import 'package:http2/transport.dart'; +import 'package:http2/src/streams/stream_handler.dart'; + +import '../test/transport_test.dart'; + +void main() { + group('transport-test', () { + transportTest('client-runs-out-of-stream-ids', + (ClientTransportConnection client, + ServerTransportConnection server) async { + Future serverFun() async { + await for (ServerTransportStream stream in server.incomingStreams) { + stream.sendHeaders([Header.ascii('x', 'y')], endStream: true); + expect(await stream.incomingMessages.toList(), hasLength(1)); + } + await server.finish(); + } + + Future clientFun() async { + var headers = [Header.ascii('a', 'b')]; + + const kMaxStreamId = StreamHandler.MAX_STREAM_ID; + for (var i = 1; i <= kMaxStreamId; i += 2) { + var stream = client.makeRequest(headers, endStream: true); + var messages = await stream.incomingMessages.toList(); + expect(messages, hasLength(1)); + } + + expect(client.isOpen, false); + expect(() => client.makeRequest(headers), + throwsA(const TypeMatcher<StateError>())); + + await Future.delayed(const Duration(seconds: 1)); + await client.finish(); + } + + var serverFuture = serverFun(); + var clientFuture = clientFun(); + + await serverFuture; + await clientFuture; + }); + }); +}
diff --git a/http2/pubspec.yaml b/http2/pubspec.yaml new file mode 100644 index 0000000..26e60b0 --- /dev/null +++ b/http2/pubspec.yaml
@@ -0,0 +1,13 @@ +name: http2 +version: 2.0.0 +description: A HTTP/2 implementation in Dart. +repository: https://github.com/dart-lang/http2 + +environment: + sdk: '>=2.12.0-0 <3.0.0' + +dev_dependencies: + build_runner: ^1.10.0 + mockito: ^5.0.0-nullsafety + test: ^1.16.0 + pedantic: ^1.10.0
diff --git a/package_config.json b/package_config.json index 67ee9a6..3b96a06 100644 --- a/package_config.json +++ b/package_config.json
@@ -237,6 +237,18 @@ "rootUri": "./glob/" }, { + "languageVersion": "2.13", + "name": "googleapis_auth", + "packageUri": "lib/", + "rootUri": "./googleapis_auth/" + }, + { + "languageVersion": "2.12", + "name": "grpc", + "packageUri": "lib/", + "rootUri": "./grpc/" + }, + { "languageVersion": "2.12", "name": "html", "packageUri": "lib/", @@ -256,6 +268,12 @@ }, { "languageVersion": "2.12", + "name": "http2", + "packageUri": "lib/", + "rootUri": "./http2/" + }, + { + "languageVersion": "2.12", "name": "http_multi_server", "packageUri": "lib/", "rootUri": "./http_multi_server/" @@ -441,6 +459,12 @@ "rootUri": "./process/" }, { + "languageVersion": "2.12", + "name": "protobuf", + "packageUri": "lib/", + "rootUri": "./protobuf/" + }, + { "languageVersion": "2.7", "name": "provider", "packageUri": "lib/",
diff --git a/protobuf/BUILD.gn b/protobuf/BUILD.gn index a61fc6a..23f9784 100644 --- a/protobuf/BUILD.gn +++ b/protobuf/BUILD.gn
@@ -1,16 +1,17 @@ -# This file is generated by importer.py for protobuf-1.1.4 +# This file is generated by package_importer.py for protobuf-2.0.1 import("//build/dart/dart_library.gni") dart_library("protobuf") { package_name = "protobuf" - language_version = "2.7" + language_version = "2.12" disable_analysis = true deps = [ "//third_party/dart-pkg/pub/fixnum", + "//third_party/dart-pkg/pub/collection", ] sources = [
diff --git a/protobuf/CHANGELOG.md b/protobuf/CHANGELOG.md index 2be6085..43350ea 100644 --- a/protobuf/CHANGELOG.md +++ b/protobuf/CHANGELOG.md
@@ -1,3 +1,12 @@ +## 2.0.1 + +* Fix bug of parsing map-values with default values. +* Merge fixes from version `1.1.2` - `1.1.4` into v2. + +## 2.0.0 + +* Stable null safety release. + ## 1.1.4 * Fix comparison of empty lists from frozen messages. @@ -116,7 +125,7 @@ ## 0.13.16+1 * Reverts `0.13.16` which accidentally introduced a breaking change, - [#284](https://github.com/dart-lang/protobuf/issues/284). This release is + [#284](https://github.com/google/protobuf.dart/issues/284). This release is identical to `0.13.15`. ## 0.13.16
diff --git a/protobuf/README.md b/protobuf/README.md index 0a48e48..25c67cd 100644 --- a/protobuf/README.md +++ b/protobuf/README.md
@@ -14,4 +14,4 @@ * [Dart generated code guide](https://developers.google.com/protocol-buffers/docs/reference/dart-generated) * [Dart API](https://pub.dev/documentation/protobuf/latest/) * [Protobuf repository](https://github.com/google/protobuf) -* [Protoc plugin project](https://github.com/dart-lang/protobuf/tree/master/protoc_plugin) +* [Protoc plugin project](https://github.com/google/protobuf.dart/tree/master/protoc_plugin)
diff --git a/protobuf/analysis_options.yaml b/protobuf/analysis_options.yaml index 88748cf..e841247 100644 --- a/protobuf/analysis_options.yaml +++ b/protobuf/analysis_options.yaml
@@ -1,5 +1,9 @@ -include: package:pedantic/analysis_options.yaml +include: package:lints/recommended.yaml linter: rules: - prefer_spread_collections: false + - comment_references + - directives_ordering + - prefer_relative_imports + - prefer_single_quotes + - prefer_spread_collections
diff --git a/protobuf/benchmarks/common.dart b/protobuf/benchmarks/common.dart index 05e6918..24a2068 100644 --- a/protobuf/benchmarks/common.dart +++ b/protobuf/benchmarks/common.dart
@@ -42,7 +42,7 @@ /// Messages deserialized from [packed]. Used in serialization benchmarks. final List<GeneratedMessage> unpacked = <GeneratedMessage>[]; - /// Create [Dataset] from a [BenchmarkDataset] proto. + /// Create [Dataset] from a `BenchmarkDataset` proto. factory Dataset.fromBinary(List<int> binary) { final dataSet = BenchmarkDataset.fromBuffer(binary); @@ -73,7 +73,7 @@ static Factories forMessage(String name) => _factories[name] ?? (throw 'Unsupported message: $name'); - /// Mapping between [BenchmarkProto.messageName] and corresponding + /// Mapping between `BenchmarkDataset.messageName` and corresponding /// deserialization factories. static final _factories = { 'benchmarks.proto2.GoogleMessage1': Factories._( @@ -93,7 +93,7 @@ fromJson: (String json) => GoogleMessage4.fromJson(json)), }; - Factories._({this.fromBuffer, this.fromJson}); + Factories._({required this.fromBuffer, required this.fromJson}); } /// Base for all protobuf benchmarks.
diff --git a/protobuf/benchmarks/d8.dart b/protobuf/benchmarks/d8.dart index d709383..f55ef70 100644 --- a/protobuf/benchmarks/d8.dart +++ b/protobuf/benchmarks/d8.dart
@@ -11,11 +11,11 @@ import 'package:js/js.dart'; /// Read the file at the given [path] and return its contents in -/// an [ArrayBuffer]. +/// an [ByteBuffer]. @JS() external ByteBuffer readbuffer(String path); -/// Read the file at the given [path] and return its contents in +/// Read the [file] and return its contents in /// a Uint8List. Uint8List readAsBytesSync(String file) { return Uint8List.view(readbuffer(file));
diff --git a/protobuf/lib/meta.dart b/protobuf/lib/meta.dart index 9ccf448..6601c02 100644 --- a/protobuf/lib/meta.dart +++ b/protobuf/lib/meta.dart
@@ -6,6 +6,8 @@ /// dart-protoc-plugin. (Experimental API; subject to change.) library protobuf.meta; +// ignore_for_file: constant_identifier_names + // List of names which cannot be used in a subclass of GeneratedMessage. const GeneratedMessage_reservedNames = <String>[ '==',
diff --git a/protobuf/lib/src/protobuf/builder_info.dart b/protobuf/lib/src/protobuf/builder_info.dart index 04df6f0..28f8b64 100644 --- a/protobuf/lib/src/protobuf/builder_info.dart +++ b/protobuf/lib/src/protobuf/builder_info.dart
@@ -16,16 +16,16 @@ final Map<int, int> oneofs = <int, int>{}; bool hasExtensions = false; bool hasRequiredFields = true; - List<FieldInfo> _sortedByTag; + List<FieldInfo>? _sortedByTag; // For well-known types. - final Object Function(GeneratedMessage message, TypeRegistry typeRegistry) + final Object? Function(GeneratedMessage message, TypeRegistry typeRegistry)? toProto3Json; final Function(GeneratedMessage targetMessage, Object json, - TypeRegistry typeRegistry, JsonParsingContext context) fromProto3Json; - final CreateBuilderFunc createEmptyInstance; + TypeRegistry typeRegistry, JsonParsingContext context)? fromProto3Json; + final CreateBuilderFunc? createEmptyInstance; - BuilderInfo(String messageName, + BuilderInfo(String? messageName, {PackageName package = const PackageName(''), this.createEmptyInstance, this.toProto3Json, @@ -35,16 +35,16 @@ void add<T>( int tagNumber, String name, - int fieldType, + int? fieldType, dynamic defaultOrMaker, - CreateBuilderFunc subBuilder, - ValueOfFunc valueOf, - List<ProtobufEnum> enumValues, - {String protoName}) { + CreateBuilderFunc? subBuilder, + ValueOfFunc? valueOf, + List<ProtobufEnum>? enumValues, + {String? protoName}) { var index = byIndex.length; final fieldInfo = (tagNumber == 0) ? FieldInfo.dummy(index) - : FieldInfo<T>(name, tagNumber, index, fieldType, + : FieldInfo<T>(name, tagNumber, index, fieldType!, defaultOrMaker: defaultOrMaker, subBuilder: subBuilder, valueOf: valueOf, @@ -56,12 +56,12 @@ void addMapField<K, V>( int tagNumber, String name, - int keyFieldType, - int valueFieldType, + int? keyFieldType, + int? valueFieldType, BuilderInfo mapEntryBuilderInfo, - CreateBuilderFunc valueCreator, - {ProtobufEnum defaultEnumValue, - String protoName}) { + CreateBuilderFunc? valueCreator, + {ProtobufEnum? defaultEnumValue, + String? protoName}) { var index = byIndex.length; _addField(MapFieldInfo<K, V>(name, tagNumber, index, PbFieldType.M, keyFieldType, valueFieldType, mapEntryBuilderInfo, valueCreator, @@ -73,11 +73,11 @@ String name, int fieldType, CheckFunc<T> check, - CreateBuilderFunc subBuilder, - ValueOfFunc valueOf, - List<ProtobufEnum> enumValues, - {ProtobufEnum defaultEnumValue, - String protoName}) { + CreateBuilderFunc? subBuilder, + ValueOfFunc? valueOf, + List<ProtobufEnum>? enumValues, + {ProtobufEnum? defaultEnumValue, + String? protoName}) { var index = byIndex.length; _addField(FieldInfo<T>.repeated( name, tagNumber, index, fieldType, check, subBuilder, @@ -89,7 +89,7 @@ void _addField(FieldInfo fi) { byIndex.add(fi); - assert(byIndex[fi.index] == fi); + assert(byIndex[fi.index!] == fi); // Fields with tag number 0 are considered dummy fields added to avoid // index calculations add up. They should not be reflected in the following // maps. @@ -102,10 +102,10 @@ void a<T>(int tagNumber, String name, int fieldType, {dynamic defaultOrMaker, - CreateBuilderFunc subBuilder, - ValueOfFunc valueOf, - List<ProtobufEnum> enumValues, - String protoName}) { + CreateBuilderFunc? subBuilder, + ValueOfFunc? valueOf, + List<ProtobufEnum>? enumValues, + String? protoName}) { add<T>(tagNumber, name, fieldType, defaultOrMaker, subBuilder, valueOf, enumValues, protoName: protoName); @@ -113,32 +113,32 @@ /// Adds PbFieldType.OS String with no default value to reduce generated /// code size. - void aOS(int tagNumber, String name, {String protoName}) { + void aOS(int tagNumber, String name, {String? protoName}) { add<String>(tagNumber, name, PbFieldType.OS, null, null, null, null, protoName: protoName); } /// Adds PbFieldType.PS String with no default value. - void pPS(int tagNumber, String name, {String protoName}) { + void pPS(int tagNumber, String name, {String? protoName}) { addRepeated<String>(tagNumber, name, PbFieldType.PS, getCheckFunction(PbFieldType.PS), null, null, null, protoName: protoName); } /// Adds PbFieldType.QS String with no default value. - void aQS(int tagNumber, String name, {String protoName}) { + void aQS(int tagNumber, String name, {String? protoName}) { add<String>(tagNumber, name, PbFieldType.QS, null, null, null, null, protoName: protoName); } /// Adds Int64 field with Int64.ZERO default. - void aInt64(int tagNumber, String name, {String protoName}) { + void aInt64(int tagNumber, String name, {String? protoName}) { add<Int64>(tagNumber, name, PbFieldType.O6, Int64.ZERO, null, null, null, protoName: protoName); } /// Adds a boolean with no default value. - void aOB(int tagNumber, String name, {String protoName}) { + void aOB(int tagNumber, String name, {String? protoName}) { add<bool>(tagNumber, name, PbFieldType.OB, null, null, null, null, protoName: protoName); } @@ -146,16 +146,16 @@ // Enum. void e<T>(int tagNumber, String name, int fieldType, {dynamic defaultOrMaker, - ValueOfFunc valueOf, - List<ProtobufEnum> enumValues, - String protoName}) { + ValueOfFunc? valueOf, + List<ProtobufEnum>? enumValues, + String? protoName}) { add<T>( tagNumber, name, fieldType, defaultOrMaker, null, valueOf, enumValues, protoName: protoName); } // Repeated, not a message, group, or enum. - void p<T>(int tagNumber, String name, int fieldType, {String protoName}) { + void p<T>(int tagNumber, String name, int fieldType, {String? protoName}) { assert(!_isGroupOrMessage(fieldType) && !_isEnum(fieldType)); addRepeated<T>(tagNumber, name, fieldType, getCheckFunction(fieldType), null, null, null, @@ -164,11 +164,11 @@ // Repeated message, group, or enum. void pc<T>(int tagNumber, String name, int fieldType, - {CreateBuilderFunc subBuilder, - ValueOfFunc valueOf, - List<ProtobufEnum> enumValues, - ProtobufEnum defaultEnumValue, - String protoName}) { + {CreateBuilderFunc? subBuilder, + ValueOfFunc? valueOf, + List<ProtobufEnum>? enumValues, + ProtobufEnum? defaultEnumValue, + String? protoName}) { assert(_isGroupOrMessage(fieldType) || _isEnum(fieldType)); addRepeated<T>(tagNumber, name, fieldType, _checkNotNull, subBuilder, valueOf, enumValues, @@ -176,7 +176,7 @@ } void aOM<T extends GeneratedMessage>(int tagNumber, String name, - {T Function() subBuilder, String protoName}) { + {T Function()? subBuilder, String? protoName}) { add<T>( tagNumber, name, @@ -189,7 +189,7 @@ } void aQM<T extends GeneratedMessage>(int tagNumber, String name, - {T Function() subBuilder, String protoName}) { + {T Function()? subBuilder, String? protoName}) { add<T>( tagNumber, name, @@ -203,20 +203,22 @@ // oneof declarations. void oo(int oneofIndex, List<int> tags) { - tags.forEach((int tag) => oneofs[tag] = oneofIndex); + for (var tag in tags) { + oneofs[tag] = oneofIndex; + } } // Map field. void m<K, V>(int tagNumber, String name, - {String entryClassName, - int keyFieldType, - int valueFieldType, - CreateBuilderFunc valueCreator, - ValueOfFunc valueOf, - List<ProtobufEnum> enumValues, - ProtobufEnum defaultEnumValue, + {String? entryClassName, + int? keyFieldType, + int? valueFieldType, + CreateBuilderFunc? valueCreator, + ValueOfFunc? valueOf, + List<ProtobufEnum>? enumValues, + ProtobufEnum? defaultEnumValue, PackageName packageName = const PackageName(''), - String protoName}) { + String? protoName}) { var mapEntryBuilderInfo = BuilderInfo(entryClassName, package: packageName) ..add(PbMap._keyFieldNumber, 'key', keyFieldType, null, null, null, null) ..add(PbMap._valueFieldNumber, 'value', valueFieldType, null, @@ -235,34 +237,34 @@ } // Returns the field name for a given tag number, for debugging purposes. - String fieldName(int tagNumber) { + String? fieldName(int tagNumber) { var i = fieldInfo[tagNumber]; - return i != null ? i.name : null; + return i?.name; } - int fieldType(int tagNumber) { + int? fieldType(int tagNumber) { var i = fieldInfo[tagNumber]; - return i != null ? i.type : null; + return i?.type; } - MakeDefaultFunc makeDefault(int tagNumber) { + MakeDefaultFunc? makeDefault(int tagNumber) { var i = fieldInfo[tagNumber]; - return i != null ? i.makeDefault : null; + return i?.makeDefault; } - CreateBuilderFunc subBuilder(int tagNumber) { + CreateBuilderFunc? subBuilder(int tagNumber) { var i = fieldInfo[tagNumber]; - return i != null ? i.subBuilder : null; + return i?.subBuilder; } - int tagNumber(String fieldName) { + int? tagNumber(String fieldName) { var i = byName[fieldName]; - return i != null ? i.tagNumber : null; + return i?.tagNumber; } - ValueOfFunc valueOfFunc(int tagNumber) { + ValueOfFunc? valueOfFunc(int tagNumber) { var i = fieldInfo[tagNumber]; - return i != null ? i.valueOf : null; + return i?.valueOf; } /// The FieldInfo for each field in tag number order. @@ -284,23 +286,23 @@ } GeneratedMessage _makeEmptyMessage( - int tagNumber, ExtensionRegistry extensionRegistry) { + int tagNumber, ExtensionRegistry? extensionRegistry) { var subBuilderFunc = subBuilder(tagNumber); if (subBuilderFunc == null && extensionRegistry != null) { subBuilderFunc = extensionRegistry - .getExtension(qualifiedMessageName, tagNumber) + .getExtension(qualifiedMessageName, tagNumber)! .subBuilder; } - return subBuilderFunc(); + return subBuilderFunc!(); } - ProtobufEnum _decodeEnum( - int tagNumber, ExtensionRegistry registry, int rawValue) { + ProtobufEnum? _decodeEnum( + int tagNumber, ExtensionRegistry? registry, int rawValue) { var f = valueOfFunc(tagNumber); if (f == null && registry != null) { - f = registry.getExtension(qualifiedMessageName, tagNumber).valueOf; + f = registry.getExtension(qualifiedMessageName, tagNumber)!.valueOf; } - return f(rawValue); + return f!(rawValue); } }
diff --git a/protobuf/lib/src/protobuf/coded_buffer.dart b/protobuf/lib/src/protobuf/coded_buffer.dart index 3709246..45c3f4e 100644 --- a/protobuf/lib/src/protobuf/coded_buffer.dart +++ b/protobuf/lib/src/protobuf/coded_buffer.dart
@@ -10,34 +10,33 @@ // https://developers.google.com/protocol-buffers/docs/encoding?hl=en#order for (var fi in fs._infosSortedByTag) { - var value = fs._values[fi.index]; + var value = fs._values[fi.index!]; if (value == null) continue; out.writeField(fi.tagNumber, fi.type, value); } if (fs._hasExtensions) { - for (var tagNumber in _sorted(fs._extensions._tagNumbers)) { - var fi = fs._extensions._getInfoOrNull(tagNumber); - out.writeField(tagNumber, fi.type, fs._extensions._getFieldOrNull(fi)); + for (var tagNumber in _sorted(fs._extensions!._tagNumbers)) { + var fi = fs._extensions!._getInfoOrNull(tagNumber)!; + out.writeField(tagNumber, fi.type, fs._extensions!._getFieldOrNull(fi)); } } if (fs._hasUnknownFields) { - fs._unknownFields.writeToCodedBufferWriter(out); + fs._unknownFields!.writeToCodedBufferWriter(out); } } -void _mergeFromCodedBufferReader( - _FieldSet fs, CodedBufferReader input, ExtensionRegistry registry) { - assert(registry != null); - +void _mergeFromCodedBufferReader(BuilderInfo meta, _FieldSet fs, + CodedBufferReader input, ExtensionRegistry registry) { + ArgumentError.checkNotNull(registry); while (true) { var tag = input.readTag(); if (tag == 0) return; var wireType = tag & 0x7; var tagNumber = tag >> 3; - var fi = fs._nonExtensionInfo(tagNumber); - fi ??= registry.getExtension(fs._messageName, tagNumber); + var fi = fs._nonExtensionInfo(meta, tagNumber); + fi ??= registry.getExtension(meta.qualifiedMessageName, tagNumber); if (fi == null || !_wireTypeMatches(fi.type, wireType)) { if (!fs._ensureUnknownFields().mergeFieldFromBuffer(tag, input)) { @@ -51,138 +50,143 @@ fieldType &= ~(PbFieldType._PACKED_BIT | PbFieldType._REQUIRED_BIT); switch (fieldType) { case PbFieldType._OPTIONAL_BOOL: - fs._setFieldUnchecked(fi, input.readBool()); + fs._setFieldUnchecked(meta, fi, input.readBool()); break; case PbFieldType._OPTIONAL_BYTES: - fs._setFieldUnchecked(fi, input.readBytes()); + fs._setFieldUnchecked(meta, fi, input.readBytes()); break; case PbFieldType._OPTIONAL_STRING: - fs._setFieldUnchecked(fi, input.readString()); + fs._setFieldUnchecked(meta, fi, input.readString()); break; case PbFieldType._OPTIONAL_FLOAT: - fs._setFieldUnchecked(fi, input.readFloat()); + fs._setFieldUnchecked(meta, fi, input.readFloat()); break; case PbFieldType._OPTIONAL_DOUBLE: - fs._setFieldUnchecked(fi, input.readDouble()); + fs._setFieldUnchecked(meta, fi, input.readDouble()); break; case PbFieldType._OPTIONAL_ENUM: var rawValue = input.readEnum(); - var value = fs._meta._decodeEnum(tagNumber, registry, rawValue); + var value = meta._decodeEnum(tagNumber, registry, rawValue); if (value == null) { var unknown = fs._ensureUnknownFields(); unknown.mergeVarintField(tagNumber, Int64(rawValue)); } else { - fs._setFieldUnchecked(fi, value); + fs._setFieldUnchecked(meta, fi, value); } break; case PbFieldType._OPTIONAL_GROUP: - var subMessage = fs._meta._makeEmptyMessage(tagNumber, registry); + var subMessage = meta._makeEmptyMessage(tagNumber, registry); var oldValue = fs._getFieldOrNull(fi); if (oldValue != null) { subMessage.mergeFromMessage(oldValue); } input.readGroup(tagNumber, subMessage, registry); - fs._setFieldUnchecked(fi, subMessage); + fs._setFieldUnchecked(meta, fi, subMessage); break; case PbFieldType._OPTIONAL_INT32: - fs._setFieldUnchecked(fi, input.readInt32()); + fs._setFieldUnchecked(meta, fi, input.readInt32()); break; case PbFieldType._OPTIONAL_INT64: - fs._setFieldUnchecked(fi, input.readInt64()); + fs._setFieldUnchecked(meta, fi, input.readInt64()); break; case PbFieldType._OPTIONAL_SINT32: - fs._setFieldUnchecked(fi, input.readSint32()); + fs._setFieldUnchecked(meta, fi, input.readSint32()); break; case PbFieldType._OPTIONAL_SINT64: - fs._setFieldUnchecked(fi, input.readSint64()); + fs._setFieldUnchecked(meta, fi, input.readSint64()); break; case PbFieldType._OPTIONAL_UINT32: - fs._setFieldUnchecked(fi, input.readUint32()); + fs._setFieldUnchecked(meta, fi, input.readUint32()); break; case PbFieldType._OPTIONAL_UINT64: - fs._setFieldUnchecked(fi, input.readUint64()); + fs._setFieldUnchecked(meta, fi, input.readUint64()); break; case PbFieldType._OPTIONAL_FIXED32: - fs._setFieldUnchecked(fi, input.readFixed32()); + fs._setFieldUnchecked(meta, fi, input.readFixed32()); break; case PbFieldType._OPTIONAL_FIXED64: - fs._setFieldUnchecked(fi, input.readFixed64()); + fs._setFieldUnchecked(meta, fi, input.readFixed64()); break; case PbFieldType._OPTIONAL_SFIXED32: - fs._setFieldUnchecked(fi, input.readSfixed32()); + fs._setFieldUnchecked(meta, fi, input.readSfixed32()); break; case PbFieldType._OPTIONAL_SFIXED64: - fs._setFieldUnchecked(fi, input.readSfixed64()); + fs._setFieldUnchecked(meta, fi, input.readSfixed64()); break; case PbFieldType._OPTIONAL_MESSAGE: - var subMessage = fs._meta._makeEmptyMessage(tagNumber, registry); + var subMessage = meta._makeEmptyMessage(tagNumber, registry); var oldValue = fs._getFieldOrNull(fi); if (oldValue != null) { subMessage.mergeFromMessage(oldValue); } input.readMessage(subMessage, registry); - fs._setFieldUnchecked(fi, subMessage); + fs._setFieldUnchecked(meta, fi, subMessage); break; case PbFieldType._REPEATED_BOOL: - _readPackable(fs, input, wireType, fi, input.readBool); + _readPackable(meta, fs, input, wireType, fi, input.readBool); break; case PbFieldType._REPEATED_BYTES: - fs._ensureRepeatedField(fi).add(input.readBytes()); + fs._ensureRepeatedField(meta, fi).add(input.readBytes()); break; case PbFieldType._REPEATED_STRING: - fs._ensureRepeatedField(fi).add(input.readString()); + fs._ensureRepeatedField(meta, fi).add(input.readString()); break; case PbFieldType._REPEATED_FLOAT: - _readPackable(fs, input, wireType, fi, input.readFloat); + _readPackable(meta, fs, input, wireType, fi, input.readFloat); break; case PbFieldType._REPEATED_DOUBLE: - _readPackable(fs, input, wireType, fi, input.readDouble); + _readPackable(meta, fs, input, wireType, fi, input.readDouble); break; case PbFieldType._REPEATED_ENUM: - _readPackableToListEnum(fs, input, wireType, fi, tagNumber, registry); + _readPackableToListEnum( + meta, fs, input, wireType, fi, tagNumber, registry); break; case PbFieldType._REPEATED_GROUP: - var subMessage = fs._meta._makeEmptyMessage(tagNumber, registry); + var subMessage = meta._makeEmptyMessage(tagNumber, registry); input.readGroup(tagNumber, subMessage, registry); - fs._ensureRepeatedField(fi).add(subMessage); + fs._ensureRepeatedField(meta, fi).add(subMessage); break; case PbFieldType._REPEATED_INT32: - _readPackable(fs, input, wireType, fi, input.readInt32); + _readPackable(meta, fs, input, wireType, fi, input.readInt32); break; case PbFieldType._REPEATED_INT64: - _readPackable(fs, input, wireType, fi, input.readInt64); + _readPackable(meta, fs, input, wireType, fi, input.readInt64); break; case PbFieldType._REPEATED_SINT32: - _readPackable(fs, input, wireType, fi, input.readSint32); + _readPackable(meta, fs, input, wireType, fi, input.readSint32); break; case PbFieldType._REPEATED_SINT64: - _readPackable(fs, input, wireType, fi, input.readSint64); + _readPackable(meta, fs, input, wireType, fi, input.readSint64); break; case PbFieldType._REPEATED_UINT32: - _readPackable(fs, input, wireType, fi, input.readUint32); + _readPackable(meta, fs, input, wireType, fi, input.readUint32); break; case PbFieldType._REPEATED_UINT64: - _readPackable(fs, input, wireType, fi, input.readUint64); + _readPackable(meta, fs, input, wireType, fi, input.readUint64); break; case PbFieldType._REPEATED_FIXED32: - _readPackable(fs, input, wireType, fi, input.readFixed32); + _readPackable(meta, fs, input, wireType, fi, input.readFixed32); break; case PbFieldType._REPEATED_FIXED64: - _readPackable(fs, input, wireType, fi, input.readFixed64); + _readPackable(meta, fs, input, wireType, fi, input.readFixed64); break; case PbFieldType._REPEATED_SFIXED32: - _readPackable(fs, input, wireType, fi, input.readSfixed32); + _readPackable(meta, fs, input, wireType, fi, input.readSfixed32); break; case PbFieldType._REPEATED_SFIXED64: - _readPackable(fs, input, wireType, fi, input.readSfixed64); + _readPackable(meta, fs, input, wireType, fi, input.readSfixed64); break; case PbFieldType._REPEATED_MESSAGE: - var subMessage = fs._meta._makeEmptyMessage(tagNumber, registry); + var subMessage = meta._makeEmptyMessage(tagNumber, registry); input.readMessage(subMessage, registry); - fs._ensureRepeatedField(fi).add(subMessage); + fs._ensureRepeatedField(meta, fi).add(subMessage); break; case PbFieldType._MAP: - fs._ensureMapField(fi)._mergeEntry(input, registry); + final mapFieldInfo = fi as MapFieldInfo; + final mapEntryMeta = mapFieldInfo.mapEntryBuilderInfo; + fs + ._ensureMapField(meta, mapFieldInfo) + ._mergeEntry(mapEntryMeta, input, registry); break; default: throw 'Unknown field type $fieldType'; @@ -190,17 +194,23 @@ } } -void _readPackable(_FieldSet fs, CodedBufferReader input, int wireType, - FieldInfo fi, Function readFunc) { +void _readPackable(BuilderInfo meta, _FieldSet fs, CodedBufferReader input, + int wireType, FieldInfo fi, Function readFunc) { void readToList(List list) => list.add(readFunc()); - _readPackableToList(fs, input, wireType, fi, readToList); + _readPackableToList(meta, fs, input, wireType, fi, readToList); } -void _readPackableToListEnum(_FieldSet fs, CodedBufferReader input, - int wireType, FieldInfo fi, int tagNumber, ExtensionRegistry registry) { +void _readPackableToListEnum( + BuilderInfo meta, + _FieldSet fs, + CodedBufferReader input, + int wireType, + FieldInfo fi, + int tagNumber, + ExtensionRegistry registry) { void readToList(List list) { var rawValue = input.readEnum(); - var value = fs._meta._decodeEnum(tagNumber, registry, rawValue); + var value = meta._decodeEnum(tagNumber, registry, rawValue); if (value == null) { var unknown = fs._ensureUnknownFields(); unknown.mergeVarintField(tagNumber, Int64(rawValue)); @@ -209,12 +219,12 @@ } } - _readPackableToList(fs, input, wireType, fi, readToList); + _readPackableToList(meta, fs, input, wireType, fi, readToList); } -void _readPackableToList(_FieldSet fs, CodedBufferReader input, int wireType, - FieldInfo fi, Function readToList) { - var list = fs._ensureRepeatedField(fi); +void _readPackableToList(BuilderInfo meta, _FieldSet fs, + CodedBufferReader input, int wireType, FieldInfo fi, Function readToList) { + var list = fs._ensureRepeatedField(meta, fi); if (wireType == WIRETYPE_LENGTH_DELIMITED) { // Packed.
diff --git a/protobuf/lib/src/protobuf/coded_buffer_reader.dart b/protobuf/lib/src/protobuf/coded_buffer_reader.dart index d61f977..7dfe56a 100644 --- a/protobuf/lib/src/protobuf/coded_buffer_reader.dart +++ b/protobuf/lib/src/protobuf/coded_buffer_reader.dart
@@ -5,7 +5,9 @@ part of protobuf; class CodedBufferReader { + // ignore: constant_identifier_names static const int DEFAULT_RECURSION_LIMIT = 64; + // ignore: constant_identifier_names static const int DEFAULT_SIZE_LIMIT = 64 << 20; final Uint8List _buffer;
diff --git a/protobuf/lib/src/protobuf/coded_buffer_writer.dart b/protobuf/lib/src/protobuf/coded_buffer_writer.dart index 21ef2c2..b1821d8 100644 --- a/protobuf/lib/src/protobuf/coded_buffer_writer.dart +++ b/protobuf/lib/src/protobuf/coded_buffer_writer.dart
@@ -2,6 +2,8 @@ // 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. +// ignore_for_file: constant_identifier_names + part of protobuf; /// Writer used for converting [GeneratedMessage]s into binary @@ -12,7 +14,7 @@ /// length-delimited representation, which means that they are represented as /// a varint encoded length followed by specified number of bytes of data. /// -/// Due to this [CodedBufferWritter] maintains two output buffers: +/// Due to this [CodedBufferWriter] maintains two output buffers: /// [_outputChunks] which contains all continuously written bytes and /// [_splices] which describes additional bytes to splice in-between /// [_outputChunks] bytes. @@ -37,14 +39,14 @@ /// Current chunk used to write data into. Once it is full it is /// pushed into [_outputChunks] and a new one is allocated. - Uint8List _outputChunk; + Uint8List? _outputChunk; /// Number of bytes written into the [_outputChunk]. int _bytesInChunk = 0; /// ByteData pointing to [_outputChunk]. Used to write primitive values /// more efficiently. - ByteData _outputChunkAsByteData; + ByteData? _outputChunkAsByteData; /// Array of pairs <Uint8List chunk, int bytesInChunk> - chunks are /// pushed into this array once they are full. @@ -174,8 +176,8 @@ /// Move the current [_outputChunk] into [_outputChunks]. /// - /// If [allocateNew] is [true] then allocate a new chunk, otherwise - /// set [_outputChunk] to null. + /// If [allocateNew] is `true` then allocate a new chunk, otherwise + /// set [_outputChunk] to `null`. void _commitChunk(bool allocateNew) { if (_bytesInChunk != 0) { _outputChunks.add(_outputChunk); @@ -186,7 +188,7 @@ if (allocateNew) { _outputChunk = Uint8List(_chunkLength); _bytesInChunk = 0; - _outputChunkAsByteData = ByteData.view(_outputChunk.buffer); + _outputChunkAsByteData = ByteData.view(_outputChunk!.buffer); } else { _outputChunk = _outputChunkAsByteData = null; _bytesInChunk = 0; @@ -242,7 +244,7 @@ } void _endLengthDelimited(int index) { - final int writtenSizeInBytes = _bytesTotal - _splices[index]; + final writtenSizeInBytes = _bytesTotal - _splices[index] as int; // Note: 0 - writtenSizeInBytes to avoid -0.0 in JavaScript. _splices[index] = 0 - writtenSizeInBytes; _bytesTotal += _varint32LengthInBytes(writtenSizeInBytes); @@ -261,10 +263,10 @@ _ensureBytes(5); var i = _bytesInChunk; while (value >= 0x80) { - _outputChunk[i++] = 0x80 | (value & 0x7f); + _outputChunk![i++] = 0x80 | (value & 0x7f); value >>= 7; } - _outputChunk[i++] = value; + _outputChunk![i++] = value; _bytesTotal += (i - _bytesInChunk); _bytesInChunk = i; } @@ -275,11 +277,11 @@ var lo = value.toUnsigned(32).toInt(); var hi = (value >> 32).toUnsigned(32).toInt(); while (hi > 0 || lo >= 0x80) { - _outputChunk[i++] = 0x80 | (lo & 0x7f); + _outputChunk![i++] = 0x80 | (lo & 0x7f); lo = (lo >> 7) | ((hi & 0x7f) << 25); hi >>= 7; } - _outputChunk[i++] = lo; + _outputChunk![i++] = lo; _bytesTotal += (i - _bytesInChunk); _bytesInChunk = i; } @@ -291,7 +293,7 @@ return; } _ensureBytes(8); - _outputChunkAsByteData.setFloat64(_bytesInChunk, value, Endian.little); + _outputChunkAsByteData!.setFloat64(_bytesInChunk, value, Endian.little); _bytesInChunk += 8; _bytesTotal += 8; } @@ -308,7 +310,7 @@ } else { const sz = 4; _ensureBytes(sz); - _outputChunkAsByteData.setFloat32(_bytesInChunk, value, Endian.little); + _outputChunkAsByteData!.setFloat32(_bytesInChunk, value, Endian.little); _bytesInChunk += sz; _bytesTotal += sz; } @@ -317,8 +319,8 @@ void _writeInt32(int value) { const sizeInBytes = 4; _ensureBytes(sizeInBytes); - _outputChunkAsByteData.setInt32( - _bytesInChunk, value & 0xFFFFFFFF, Endian.little); + _outputChunkAsByteData! + .setInt32(_bytesInChunk, value & 0xFFFFFFFF, Endian.little); _bytesInChunk += sizeInBytes; _bytesTotal += sizeInBytes; }
diff --git a/protobuf/lib/src/protobuf/extension.dart b/protobuf/lib/src/protobuf/extension.dart index cecc257..17343fc 100644 --- a/protobuf/lib/src/protobuf/extension.dart +++ b/protobuf/lib/src/protobuf/extension.dart
@@ -10,10 +10,10 @@ Extension(this.extendee, String name, int tagNumber, int fieldType, {dynamic defaultOrMaker, - CreateBuilderFunc subBuilder, - ValueOfFunc valueOf, - List<ProtobufEnum> enumValues, - String protoName}) + CreateBuilderFunc? subBuilder, + ValueOfFunc? valueOf, + List<ProtobufEnum>? enumValues, + String? protoName}) : super(name, tagNumber, null, fieldType, defaultOrMaker: defaultOrMaker, subBuilder: subBuilder, @@ -22,11 +22,11 @@ protoName: protoName); Extension.repeated(this.extendee, String name, int tagNumber, int fieldType, - {CheckFunc<T> check, - CreateBuilderFunc subBuilder, - ValueOfFunc valueOf, - List<ProtobufEnum> enumValues, - String protoName}) + {CheckFunc<T>? check, + CreateBuilderFunc? subBuilder, + ValueOfFunc? valueOf, + List<ProtobufEnum>? enumValues, + String? protoName}) : super.repeated(name, tagNumber, null, fieldType, check, subBuilder, valueOf: valueOf, enumValues: enumValues, protoName: protoName); @@ -37,7 +37,7 @@ bool operator ==(other) { if (other is! Extension) return false; - Extension o = other; + var o = other; return extendee == o.extendee && tagNumber == o.tagNumber; } }
diff --git a/protobuf/lib/src/protobuf/extension_field_set.dart b/protobuf/lib/src/protobuf/extension_field_set.dart index 8998042..2497e03 100644 --- a/protobuf/lib/src/protobuf/extension_field_set.dart +++ b/protobuf/lib/src/protobuf/extension_field_set.dart
@@ -12,7 +12,7 @@ _ExtensionFieldSet(this._parent); - Extension _getInfoOrNull(int tagNumber) => _info[tagNumber]; + Extension? _getInfoOrNull(int? tagNumber) => _info[tagNumber]; dynamic _getFieldOrDefault(Extension fi) { if (fi.isRepeated) return _getList(fi); @@ -23,7 +23,7 @@ var value = _getFieldOrNull(fi); if (value == null) { _checkNotInUnknown(fi); - return fi.makeDefault(); + return fi.makeDefault!(); } return value; } @@ -39,7 +39,7 @@ /// /// If it doesn't exist, creates the list and saves the extension. /// Suitable for public API and decoders. - List<T> _ensureRepeatedField<T>(Extension<T> fi) { + List<T?> _ensureRepeatedField<T>(Extension<T?> fi) { assert(!_isReadOnly); assert(fi.isRepeated); assert(fi.extendee == '' || fi.extendee == _parent._messageName); @@ -47,20 +47,20 @@ var list = _values[fi.tagNumber]; if (list != null) return list as List<T>; - return _addInfoAndCreateList(fi); + return _addInfoAndCreateList(fi) as List<T?>; } - List<T> _getList<T>(Extension<T> fi) { + List<T?> _getList<T>(Extension<T?> fi) { var value = _values[fi.tagNumber]; if (value != null) return value as List<T>; _checkNotInUnknown(fi); if (_isReadOnly) return List<T>.unmodifiable(const []); - return _addInfoAndCreateList(fi); + return _addInfoAndCreateList(fi) as List<T?>; } List _addInfoAndCreateList(Extension fi) { _validateInfo(fi); - var newList = fi._createRepeatedField(_parent._message); + var newList = fi._createRepeatedField(_parent._message!); _addInfoUnchecked(fi); _setFieldUnchecked(fi, newList); return newList; @@ -76,7 +76,7 @@ void _clearField(Extension fi) { _ensureWritable(); _validateInfo(fi); - if (_parent._hasObservers) _parent._eventPlugin.beforeClearField(fi); + if (_parent._hasObservers) _parent._eventPlugin!.beforeClearField(fi); _values.remove(fi.tagNumber); } @@ -130,7 +130,7 @@ void _setFieldUnchecked(Extension fi, value) { if (_parent._hasObservers) { - _parent._eventPlugin.beforeSetField(fi, value); + _parent._eventPlugin!.beforeSetField(fi, value); } _values[fi.tagNumber] = value; } @@ -142,7 +142,7 @@ bool get _hasValues => _values.isNotEmpty; - bool _equalValues(_ExtensionFieldSet other) => + bool _equalValues(_ExtensionFieldSet? other) => other != null && _areMapsEqual(_values, other._values); void _clearValues() => _values.clear(); @@ -153,7 +153,7 @@ /// Extensions cannot contain map fields. void _shallowCopyValues(_ExtensionFieldSet original) { for (var tagNumber in original._tagNumbers) { - var extension = original._getInfoOrNull(tagNumber); + var extension = original._getInfoOrNull(tagNumber)!; _addInfoUnchecked(extension); final value = original._getFieldOrNull(extension); @@ -191,7 +191,7 @@ void _checkNotInUnknown(Extension extension) { if (_parent._hasUnknownFields && - _parent._unknownFields.hasField(extension.tagNumber)) { + _parent._unknownFields!.hasField(extension.tagNumber)) { throw StateError( 'Trying to get $extension that is present as an unknown field. ' 'Parse the message with this extension in the extension registry or '
diff --git a/protobuf/lib/src/protobuf/extension_registry.dart b/protobuf/lib/src/protobuf/extension_registry.dart index 9daeb1a..be1f3d1 100644 --- a/protobuf/lib/src/protobuf/extension_registry.dart +++ b/protobuf/lib/src/protobuf/extension_registry.dart
@@ -10,6 +10,7 @@ final Map<String, Map<int, Extension>> _extensions = <String, Map<int, Extension>>{}; + // ignore: constant_identifier_names static const ExtensionRegistry EMPTY = _EmptyExtensionRegistry(); /// Stores an [extension] in the registry. @@ -26,7 +27,7 @@ /// Retrieves an extension from the registry that adds tag number [tagNumber] /// to the [messageName] message type. - Extension getExtension(String messageName, int tagNumber) { + Extension? getExtension(String messageName, int tagNumber) { var map = _extensions[messageName]; if (map != null) { return map[tagNumber]; @@ -34,7 +35,7 @@ return null; } - /// Returns a shallow copy of [message], with all extensions in [this] parsed + /// Returns a shallow copy of [message], with all extensions in `this` parsed /// from the unknown fields of [message] and of every nested submessage. /// /// Extensions already present in [message] will be preserved. @@ -92,18 +93,18 @@ T _reparseMessage<T extends GeneratedMessage>( T message, ExtensionRegistry extensionRegistry) { - T result; + T? result; T ensureResult() { if (result == null) { - result ??= message.createEmptyInstance(); - result._fieldSet._shallowCopyValues(message._fieldSet); + result ??= message.info_.createEmptyInstance!() as T; + result!._fieldSet._shallowCopyValues(message._fieldSet); } - return result; + return result!; } - UnknownFieldSet resultUnknownFields; + UnknownFieldSet? resultUnknownFields; UnknownFieldSet ensureUnknownFields() => - resultUnknownFields ??= ensureResult()._fieldSet._unknownFields; + resultUnknownFields ??= ensureResult()._fieldSet._unknownFields!; var messageUnknownFields = message._fieldSet._unknownFields; if (messageUnknownFields != null) { @@ -123,18 +124,18 @@ } } - message._fieldSet._meta.byIndex.forEach((FieldInfo field) { - PbList resultEntries; + for (var field in message._fieldSet._meta.byIndex) { + PbList? resultEntries; PbList ensureEntries() => - resultEntries ??= ensureResult()._fieldSet._values[field.index]; + resultEntries ??= ensureResult()._fieldSet._values[field.index!]; - PbMap resultMap; + PbMap? resultMap; PbMap ensureMap() => - resultMap ??= ensureResult()._fieldSet._values[field.index]; + resultMap ??= ensureResult()._fieldSet._values[field.index!]; if (field.isRepeated) { - final messageEntries = message._fieldSet._values[field.index]; - if (messageEntries == null) return; + final messageEntries = message._fieldSet._values[field.index!]; + if (messageEntries == null) continue; if (field.isGroupOrMessage) { for (var i = 0; i < messageEntries.length; i++) { final GeneratedMessage entry = messageEntries[i]; @@ -145,9 +146,9 @@ } } } else if (field is MapFieldInfo) { - final messageMap = message._fieldSet._values[field.index]; - if (messageMap == null) return; - if (_isGroupOrMessage(field.valueFieldType)) { + final messageMap = message._fieldSet._values[field.index!]; + if (messageMap == null) continue; + if (_isGroupOrMessage(field.valueFieldType!)) { for (var key in messageMap.keys) { final GeneratedMessage value = messageMap[key]; final reparsedValue = _reparseMessage(value, extensionRegistry); @@ -157,18 +158,18 @@ } } } else if (field.isGroupOrMessage) { - final messageSubField = message._fieldSet._values[field.index]; - if (messageSubField == null) return; + final messageSubField = message._fieldSet._values[field.index!]; + if (messageSubField == null) continue; final reparsedSubField = _reparseMessage<GeneratedMessage>(messageSubField, extensionRegistry); if (!identical(messageSubField, reparsedSubField)) { - ensureResult()._fieldSet._values[field.index] = reparsedSubField; + ensureResult()._fieldSet._values[field.index!] = reparsedSubField; } } - }); + } if (result != null && message.isFrozen) { - result.freeze(); + result!.freeze(); } return result ?? message; @@ -192,7 +193,7 @@ } @override - Extension getExtension(String messageName, int tagNumber) => null; + Extension? getExtension(String messageName, int tagNumber) => null; @override T reparseMessage<T extends GeneratedMessage>(T message) =>
diff --git a/protobuf/lib/src/protobuf/field_error.dart b/protobuf/lib/src/protobuf/field_error.dart index 54e04be..d6c5e43 100644 --- a/protobuf/lib/src/protobuf/field_error.dart +++ b/protobuf/lib/src/protobuf/field_error.dart
@@ -9,7 +9,7 @@ /// /// For enums, group, and message fields, this check is only approximate, /// because the exact type isn't included in [fieldType]. -String _getFieldError(int fieldType, var value) { +String? _getFieldError(int fieldType, var value) { switch (PbFieldType._baseType(fieldType)) { case PbFieldType._BOOL_BIT: if (value is! bool) return 'not type bool'; @@ -51,7 +51,7 @@ case PbFieldType._SFIXED64_BIT: // We always use the full range of the same Dart type. // It's up to the caller to treat the Int64 as signed or unsigned. - // See: https://github.com/dart-lang/protobuf/issues/44 + // See: https://github.com/google/protobuf.dart/issues/44 if (value is! Int64) return 'not Int64'; return null; @@ -92,7 +92,7 @@ case PbFieldType._FIXED64_BIT: // We always use the full range of the same Dart type. // It's up to the caller to treat the Int64 as signed or unsigned. - // See: https://github.com/dart-lang/protobuf/issues/44 + // See: https://github.com/google/protobuf.dart/issues/44 return _checkNotNull; case PbFieldType._FLOAT_BIT: @@ -112,22 +112,24 @@ // check functions for repeated fields -void _checkNotNull(Object val) { +void _checkNotNull(Object? val) { if (val == null) { throw ArgumentError("Can't add a null to a repeated field"); } } -void _checkFloat(Object val) { - if (!_isFloat32(val)) throw _createFieldRangeError(val, 'a float'); +void _checkFloat(Object? val) { + if (!_isFloat32(val as double)) throw _createFieldRangeError(val, 'a float'); } -void _checkSigned32(Object val) { - if (!_isSigned32(val)) throw _createFieldRangeError(val, 'a signed int32'); +void _checkSigned32(Object? val) { + if (!_isSigned32(val as int)) { + throw _createFieldRangeError(val, 'a signed int32'); + } } -void _checkUnsigned32(Object val) { - if (!_isUnsigned32(val)) { +void _checkUnsigned32(Object? val) { + if (!_isUnsigned32(val as int)) { throw _createFieldRangeError(val, 'an unsigned int32'); } }
diff --git a/protobuf/lib/src/protobuf/field_info.dart b/protobuf/lib/src/protobuf/field_info.dart index 709f645..9d9deb3 100644 --- a/protobuf/lib/src/protobuf/field_info.dart +++ b/protobuf/lib/src/protobuf/field_info.dart
@@ -6,7 +6,7 @@ /// An object representing a protobuf message field. class FieldInfo<T> { - FrozenPbList<T> _emptyList; + FrozenPbList<T>? _emptyList; /// Name of this field as the `json_name` reported by protoc. /// @@ -19,34 +19,34 @@ final String protoName; final int tagNumber; - final int index; // index of the field's value. Null for extensions. + final int? index; // index of the field's value. Null for extensions. final int type; // Constructs the default value of a field. // (Only used for repeated fields where check is null.) - final MakeDefaultFunc makeDefault; + final MakeDefaultFunc? makeDefault; // Creates an empty message or group when decoding a message. // Not used for other types. // see GeneratedMessage._getEmptyMessage - final CreateBuilderFunc subBuilder; + final CreateBuilderFunc? subBuilder; // List of all enum enumValues. // (Not used for other types.) - final List<ProtobufEnum> enumValues; + final List<ProtobufEnum>? enumValues; // Default enum value, if type is a PbList<ProtobufEnum> or a // PbMap<[anything], ProtobufEnum>. - final ProtobufEnum defaultEnumValue; + final ProtobufEnum? defaultEnumValue; // Looks up the enum value given its integer code. // (Not used for other types.) // see GeneratedMessage._getValueOfFunc - final ValueOfFunc valueOf; + final ValueOfFunc? valueOf; // Verifies an item being added to a repeated field // (Not used for non-repeated fields.) - final CheckFunc<T> check; + final CheckFunc<T>? check; FieldInfo(this.name, this.tagNumber, this.index, this.type, {dynamic defaultOrMaker, @@ -54,7 +54,7 @@ this.valueOf, this.enumValues, this.defaultEnumValue, - String protoName}) + String? protoName}) : makeDefault = findMakeDefault(type, defaultOrMaker), check = null, protoName = protoName ?? _unCamelCase(name), @@ -79,17 +79,17 @@ FieldInfo.repeated(this.name, this.tagNumber, this.index, this.type, this.check, this.subBuilder, - {this.valueOf, this.enumValues, this.defaultEnumValue, String protoName}) - : makeDefault = (() => PbList<T>(check: check)), + {this.valueOf, this.enumValues, this.defaultEnumValue, String? protoName}) + : makeDefault = (() => PbList<T>(check: check!)), protoName = protoName ?? _unCamelCase(name) { - assert(name != null); - assert(tagNumber != null); + ArgumentError.checkNotNull(name, 'name'); + ArgumentError.checkNotNull(tagNumber, 'tagNumber'); assert(_isRepeated(type)); assert(check != null); assert(!_isEnum(type) || valueOf != null); } - static MakeDefaultFunc findMakeDefault(int type, dynamic defaultOrMaker) { + static MakeDefaultFunc? findMakeDefault(int type, dynamic defaultOrMaker) { if (defaultOrMaker == null) return PbFieldType._defaultForType(type); if (defaultOrMaker is MakeDefaultFunc) return defaultOrMaker; return () => defaultOrMaker; @@ -111,7 +111,7 @@ if (isRepeated) { return _emptyList ??= FrozenPbList._([]); } - return makeDefault(); + return makeDefault!(); } /// Returns true if the field's value is okay to transmit. @@ -169,7 +169,7 @@ /// /// Delegates actual list creation to the message, so that it can /// be overridden by a mixin. - List<T> _createRepeatedField(GeneratedMessage m) { + List<T?> _createRepeatedField(GeneratedMessage m) { assert(isRepeated); return m.createRepeatedField<T>(tagNumber, this); } @@ -177,13 +177,13 @@ /// Same as above, but allow a tighter typed List to be created. List<S> _createRepeatedFieldWithType<S extends T>(GeneratedMessage m) { assert(isRepeated); - return m.createRepeatedField<S>(tagNumber, this); + return m.createRepeatedField<S>(tagNumber, this as FieldInfo<S>); } /// Convenience method to thread this FieldInfo's reified type parameter to /// _FieldSet._ensureRepeatedField. - List<T> _ensureRepeatedField(_FieldSet fs) { - return fs._ensureRepeatedField<T>(this); + List<T?> _ensureRepeatedField(BuilderInfo meta, _FieldSet fs) { + return fs._ensureRepeatedField<T>(meta, this); } @override @@ -194,17 +194,17 @@ String _unCamelCase(String name) { return name.replaceAllMapped( - _upperCase, (match) => '_${match.group(0).toLowerCase()}'); + _upperCase, (match) => '_${match.group(0)!.toLowerCase()}'); } -class MapFieldInfo<K, V> extends FieldInfo<PbMap<K, V>> { - final int keyFieldType; - final int valueFieldType; +class MapFieldInfo<K, V> extends FieldInfo<PbMap<K, V>?> { + final int? keyFieldType; + final int? valueFieldType; /// Creates a new empty instance of the value type. /// /// `null` if the value type is not a Message type. - final CreateBuilderFunc valueCreator; + final CreateBuilderFunc? valueCreator; final BuilderInfo mapEntryBuilderInfo; @@ -217,24 +217,24 @@ this.valueFieldType, this.mapEntryBuilderInfo, this.valueCreator, - {ProtobufEnum defaultEnumValue, - String protoName}) + {ProtobufEnum? defaultEnumValue, + String? protoName}) : super(name, tagNumber, index, type, defaultOrMaker: () => PbMap<K, V>(keyFieldType, valueFieldType, mapEntryBuilderInfo), defaultEnumValue: defaultEnumValue, protoName: protoName) { - assert(name != null); - assert(tagNumber != null); + ArgumentError.checkNotNull(name, 'name'); + ArgumentError.checkNotNull(tagNumber, 'tagNumber'); assert(_isMapField(type)); assert(!_isEnum(type) || valueOf != null); } FieldInfo get valueFieldInfo => - mapEntryBuilderInfo.fieldInfo[PbMap._valueFieldNumber]; + mapEntryBuilderInfo.fieldInfo[PbMap._valueFieldNumber]!; - Map<K, V> _ensureMapField(_FieldSet fs) { - return fs._ensureMapField<K, V>(this); + Map<K, V> _ensureMapField(BuilderInfo meta, _FieldSet fs) { + return fs._ensureMapField<K, V>(meta, this); } Map<K, V> _createMapField(GeneratedMessage m) {
diff --git a/protobuf/lib/src/protobuf/field_set.dart b/protobuf/lib/src/protobuf/field_set.dart index 018f332..f137aad 100644 --- a/protobuf/lib/src/protobuf/field_set.dart +++ b/protobuf/lib/src/protobuf/field_set.dart
@@ -5,10 +5,10 @@ part of protobuf; typedef FrozenMessageErrorHandler = void Function(String messageName, - [String methodName]); + [String? methodName]); void defaultFrozenMessageModificationHandler(String messageName, - [String methodName]) { + [String? methodName]) { if (methodName != null) { throw UnsupportedError( 'Attempted to call $methodName on a read-only message ($messageName)'); @@ -48,9 +48,8 @@ /// polymorphic access due to inheritance. This turns out to /// be faster when compiled to JavaScript. class _FieldSet { - final GeneratedMessage _message; - final BuilderInfo _meta; - final EventPlugin _eventPlugin; + final GeneratedMessage? _message; + final EventPlugin? _eventPlugin; /// The value of each non-extension field in a fixed-length array. /// The index of a field can be found in [FieldInfo.index]. @@ -58,10 +57,10 @@ final List _values; /// Contains all the extension fields, or null if there aren't any. - _ExtensionFieldSet _extensions; + _ExtensionFieldSet? _extensions; /// Contains all the unknown fields, or null if there aren't any. - UnknownFieldSet _unknownFields; + UnknownFieldSet? _unknownFields; /// Encodes whether `this` has been frozen, and if so, also memoizes the /// hash code. @@ -74,12 +73,18 @@ /// code as an `int`. Object _frozenState = false; + /// The [BuilderInfo] for the [GeneratedMessage] this [_FieldSet] belongs to. + /// + /// WARNING: Avoid calling this for any performance critical code, instead + /// obtain the [BuilderInfo] on the call site. + BuilderInfo get _meta => _message!.info_; + /// Returns the value of [_frozenState] as if it were a boolean indicator /// for whether `this` is read-only (has been frozen). /// /// If the value is not a `bool`, then it must contain the memoized hash code /// value, in which case the proto must be read-only. - bool get _isReadOnly => _frozenState is bool ? _frozenState : true; + bool get _isReadOnly => _frozenState is bool ? _frozenState as bool : true; /// Returns the value of [_frozenState] if it contains the pre-computed value /// of the hashCode for the frozen field sets. @@ -90,15 +95,15 @@ /// /// If [_frozenState] contains a boolean, the hashCode hasn't been memoized, /// so it will return null. - int get _memoizedHashCode => _frozenState is int ? _frozenState : null; + int? get _memoizedHashCode => + _frozenState is int ? _frozenState as int : null; // Maps a oneof decl index to the tag number which is currently set. If the // index is not present, the oneof field is unset. - final Map<int, int> _oneofCases; + final Map<int, int>? _oneofCases; _FieldSet(this._message, BuilderInfo meta, this._eventPlugin) - : _meta = meta, - _values = _makeValueList(meta.byIndex.length), + : _values = _makeValueList(meta.byIndex.length), _oneofCases = meta.oneofs.isEmpty ? null : <int, int>{}; static List _makeValueList(int length) { @@ -122,14 +127,14 @@ Iterable<FieldInfo> get _infosSortedByTag => _meta.sortedByTag; /// Returns true if we should send events to the plugin. - bool get _hasObservers => _eventPlugin != null && _eventPlugin.hasObservers; + bool get _hasObservers => _eventPlugin != null && _eventPlugin!.hasObservers; bool get _hasExtensions => _extensions != null; bool get _hasUnknownFields => _unknownFields != null; _ExtensionFieldSet _ensureExtensions() { if (!_hasExtensions) _extensions = _ExtensionFieldSet(this); - return _extensions; + return _extensions!; } UnknownFieldSet _ensureUnknownFields() { @@ -137,13 +142,14 @@ if (_isReadOnly) return UnknownFieldSet.emptyUnknownFieldSet; _unknownFields = UnknownFieldSet(); } - return _unknownFields; + return _unknownFields!; } // Metadata about single fields /// Returns FieldInfo for a non-extension field, or null if not found. - FieldInfo _nonExtensionInfo(int tagNumber) => _meta.fieldInfo[tagNumber]; + FieldInfo? _nonExtensionInfo(BuilderInfo meta, int? tagNumber) => + meta.fieldInfo[tagNumber]; /// Returns FieldInfo for a non-extension field. FieldInfo _nonExtensionInfoByIndex(int index) => _meta.byIndex[index]; @@ -157,11 +163,11 @@ } /// Returns the FieldInfo for a regular or extension field. - FieldInfo _getFieldInfoOrNull(int tagNumber) { - var fi = _nonExtensionInfo(tagNumber); + FieldInfo? _getFieldInfoOrNull(int tagNumber) { + var fi = _nonExtensionInfo(_meta, tagNumber); if (fi != null) return fi; if (!_hasExtensions) return null; - return _extensions._getInfoOrNull(tagNumber); + return _extensions!._getInfoOrNull(tagNumber); } void _markReadOnly() { @@ -169,20 +175,20 @@ _frozenState = true; for (var field in _meta.sortedByTag) { if (field.isRepeated) { - final entries = _values[field.index]; + final entries = _values[field.index!]; if (entries == null) continue; if (field.isGroupOrMessage) { for (var subMessage in entries as List<GeneratedMessage>) { subMessage.freeze(); } } - _values[field.index] = entries.toFrozenPbList(); + _values[field.index!] = entries.toFrozenPbList(); } else if (field.isMapField) { - PbMap map = _values[field.index]; + PbMap? map = _values[field.index!]; if (map == null) continue; - _values[field.index] = map.freeze(); + _values[field.index!] = map.freeze(); } else if (field.isGroupOrMessage) { - final entry = _values[field.index]; + final entry = _values[field.index!]; if (entry != null) { (entry as GeneratedMessage).freeze(); } @@ -209,30 +215,30 @@ /// Creates repeated fields (unless read-only). /// Suitable for public API. dynamic _getField(int tagNumber) { - var fi = _nonExtensionInfo(tagNumber); + var fi = _nonExtensionInfo(_meta, tagNumber); if (fi != null) { - var value = _values[fi.index]; + var value = _values[fi.index!]; if (value != null) return value; return _getDefault(fi); } if (_hasExtensions) { - var fi = _extensions._getInfoOrNull(tagNumber); + var fi = _extensions!._getInfoOrNull(tagNumber); if (fi != null) { - return _extensions._getFieldOrDefault(fi); + return _extensions!._getFieldOrDefault(fi); } } throw ArgumentError('tag $tagNumber not defined in $_messageName'); } dynamic _getDefault(FieldInfo fi) { - if (!fi.isRepeated) return fi.makeDefault(); + if (!fi.isRepeated) return fi.makeDefault!(); if (_isReadOnly) return fi.readonlyDefault; // TODO(skybrian) we could avoid this by generating another // method for repeated fields: // msg.mutableFoo().add(123); - var value = fi._createRepeatedField(_message); - _setNonExtensionFieldUnchecked(fi, value); + var value = fi._createRepeatedField(_message!); + _setNonExtensionFieldUnchecked(_meta, fi, value); return value; } @@ -243,8 +249,8 @@ // TODO(skybrian) we could avoid this by generating another // method for repeated fields: // msg.mutableFoo().add(123); - var value = fi._createRepeatedFieldWithType<T>(_message); - _setNonExtensionFieldUnchecked(fi, value); + var value = fi._createRepeatedFieldWithType<T>(_message!); + _setNonExtensionFieldUnchecked(_meta, fi, value); return value; } @@ -252,12 +258,12 @@ assert(fi.isMapField); if (_isReadOnly) { - return PbMap<K, V>.unmodifiable(PbMap<K, V>( - fi.keyFieldType, fi.valueFieldType, fi.mapEntryBuilderInfo)); + return PbMap<K, V>.unmodifiable( + PbMap<K, V>(fi.keyFieldType, fi.valueFieldType)); } - var value = fi._createMapField(_message); - _setNonExtensionFieldUnchecked(fi, value); + var value = fi._createMapField(_message!); + _setNonExtensionFieldUnchecked(_meta, fi, value); return value; } @@ -272,39 +278,40 @@ /// Works for both extended and non-extend fields. /// Works for both repeated and non-repeated fields. dynamic _getFieldOrNull(FieldInfo fi) { - if (fi.index != null) return _values[fi.index]; + if (fi.index != null) return _values[fi.index!]; if (!_hasExtensions) return null; - return _extensions._getFieldOrNull(fi); + return _extensions!._getFieldOrNull(fi as Extension<dynamic>); } bool _hasField(int tagNumber) { - var fi = _nonExtensionInfo(tagNumber); - if (fi != null) return _$has(fi.index); + var fi = _nonExtensionInfo(_meta, tagNumber); + if (fi != null) return _$has(fi.index!); if (!_hasExtensions) return false; - return _extensions._hasField(tagNumber); + return _extensions!._hasField(tagNumber); } - void _clearField(int tagNumber) { + void _clearField(int? tagNumber) { _ensureWritable(); - var fi = _nonExtensionInfo(tagNumber); + final meta = _meta; + var fi = _nonExtensionInfo(meta, tagNumber); if (fi != null) { // clear a non-extension field - if (_hasObservers) _eventPlugin.beforeClearField(fi); - _values[fi.index] = null; + if (_hasObservers) _eventPlugin!.beforeClearField(fi); + _values[fi.index!] = null; - if (_meta.oneofs.containsKey(fi.tagNumber)) { - _oneofCases.remove(_meta.oneofs[fi.tagNumber]); + if (meta.oneofs.containsKey(fi.tagNumber)) { + _oneofCases!.remove(meta.oneofs[fi.tagNumber]); } - var oneofIndex = _meta.oneofs[fi.tagNumber]; - if (oneofIndex != null) _oneofCases[oneofIndex] = 0; + var oneofIndex = meta.oneofs[fi.tagNumber]; + if (oneofIndex != null) _oneofCases![oneofIndex] = 0; return; } if (_hasExtensions) { - var fi = _extensions._getInfoOrNull(tagNumber); + var fi = _extensions!._getInfoOrNull(tagNumber); if (fi != null) { - _extensions._clearField(fi); + _extensions!._clearField(fi); return; } } @@ -317,15 +324,16 @@ /// /// Works for both extended and non-extended fields. /// Suitable for public API. - void _setField(int tagNumber, value) { - if (value == null) throw ArgumentError('value is null'); + void _setField(int tagNumber, Object value) { + ArgumentError.checkNotNull(value, 'value'); - var fi = _nonExtensionInfo(tagNumber); + final meta = _meta; + var fi = _nonExtensionInfo(meta, tagNumber); if (fi == null) { if (!_hasExtensions) { throw ArgumentError('tag $tagNumber not defined in $_messageName'); } - _extensions._setField(tagNumber, value); + _extensions!._setField(tagNumber, value); return; } @@ -334,22 +342,22 @@ fi, value, 'repeating field (use get + .add())')); } _validateField(fi, value); - _setNonExtensionFieldUnchecked(fi, value); + _setNonExtensionFieldUnchecked(meta, fi, value); } /// Sets a non-repeated field without validating it. /// /// Works for both extended and non-extended fields. /// Suitable for decoders that do their own validation. - void _setFieldUnchecked(FieldInfo fi, value) { - assert(fi != null); + void _setFieldUnchecked(BuilderInfo meta, FieldInfo fi, value) { + ArgumentError.checkNotNull(fi, 'fi'); assert(!fi.isRepeated); if (fi.index == null) { _ensureExtensions() - .._addInfoUnchecked(fi) + .._addInfoUnchecked(fi as Extension<dynamic>) .._setFieldUnchecked(fi, value); } else { - _setNonExtensionFieldUnchecked(fi, value); + _setNonExtensionFieldUnchecked(meta, fi, value); } } @@ -359,40 +367,40 @@ /// Creates and stores the repeated field if it doesn't exist. /// If it's an extension and the list doesn't exist, validates and stores it. /// Suitable for decoders. - List<T> _ensureRepeatedField<T>(FieldInfo<T> fi) { + List<T?> _ensureRepeatedField<T>(BuilderInfo meta, FieldInfo<T> fi) { assert(!_isReadOnly); assert(fi.isRepeated); if (fi.index == null) { - return _ensureExtensions()._ensureRepeatedField(fi); + return _ensureExtensions()._ensureRepeatedField(fi as Extension<T>); } var value = _getFieldOrNull(fi); if (value != null) return value as List<T>; - var newValue = fi._createRepeatedField(_message); - _setNonExtensionFieldUnchecked(fi, newValue); + var newValue = fi._createRepeatedField(_message!); + _setNonExtensionFieldUnchecked(meta, fi, newValue); return newValue; } - PbMap<K, V> _ensureMapField<K, V>(MapFieldInfo<K, V> fi) { + PbMap<K, V> _ensureMapField<K, V>(BuilderInfo meta, MapFieldInfo<K, V> fi) { assert(!_isReadOnly); assert(fi.isMapField); assert(fi.index != null); // Map fields are not allowed to be extensions. var value = _getFieldOrNull(fi); - if (value != null) return value as Map<K, V>; + if (value != null) return (value as Map<K, V>) as PbMap<K, V>; - var newValue = fi._createMapField(_message); - _setNonExtensionFieldUnchecked(fi, newValue); - return newValue; + var newValue = fi._createMapField(_message!); + _setNonExtensionFieldUnchecked(meta, fi, newValue); + return newValue as PbMap<K, V>; } /// Sets a non-extended field and fires events. - void _setNonExtensionFieldUnchecked(FieldInfo fi, value) { + void _setNonExtensionFieldUnchecked(BuilderInfo meta, FieldInfo fi, value) { var tag = fi.tagNumber; - var oneofIndex = _meta.oneofs[tag]; + var oneofIndex = meta.oneofs[tag]; if (oneofIndex != null) { - _clearField(_oneofCases[oneofIndex]); - _oneofCases[oneofIndex] = tag; + _clearField(_oneofCases![oneofIndex]); + _oneofCases![oneofIndex] = tag; } // It is important that the callback to the observers is not moved to the @@ -400,15 +408,15 @@ // Otherwise the observers will be notified about 'clearField' and // 'setField' events in an incorrect order. if (_hasObservers) { - _eventPlugin.beforeSetField(fi, value); + _eventPlugin!.beforeSetField(fi, value); } - _values[fi.index] = value; + _values[fi.index!] = value; } // Generated method implementations /// The implementation of a generated getter. - T _$get<T>(int index, T defaultValue) { + T _$get<T>(int index, T? defaultValue) { var value = _values[index]; if (value != null) return value as T; if (defaultValue != null) return defaultValue; @@ -426,7 +434,7 @@ T _$ensure<T>(int index) { if (!_$has(index)) { - dynamic value = _nonExtensionInfoByIndex(index).subBuilder(); + dynamic value = _nonExtensionInfoByIndex(index).subBuilder!(); _$set(index, value); return value; } @@ -439,18 +447,19 @@ List<T> _$getList<T>(int index) { var value = _values[index]; if (value != null) return value as List<T>; - return _getDefaultList<T>(_nonExtensionInfoByIndex(index)); + return _getDefaultList<T>(_nonExtensionInfoByIndex(index) as FieldInfo<T>); } /// The implementation of a generated getter for map fields. - Map<K, V> _$getMap<K, V>(int index) { + Map<K, V> _$getMap<K, V>(GeneratedMessage parentMessage, int index) { var value = _values[index]; if (value != null) return value as Map<K, V>; - return _getDefaultMap<K, V>(_nonExtensionInfoByIndex(index)); + return _getDefaultMap<K, V>( + _nonExtensionInfoByIndex(index) as MapFieldInfo<K, V>); } /// The implementation of a generated getter for `bool` fields. - bool _$getB(int index, bool defaultValue) { + bool _$getB(int index, bool? defaultValue) { var value = _values[index]; if (value == null) { if (defaultValue != null) return defaultValue; @@ -470,7 +479,7 @@ } /// The implementation of a generated getter for int fields. - int _$getI(int index, int defaultValue) { + int _$getI(int index, int? defaultValue) { var value = _values[index]; if (value == null) { if (defaultValue != null) return defaultValue; @@ -490,7 +499,7 @@ } /// The implementation of a generated getter for String fields. - String _$getS(int index, String defaultValue) { + String _$getS(int index, String? defaultValue) { var value = _values[index]; if (value == null) { if (defaultValue != null) return defaultValue; @@ -530,7 +539,7 @@ /// In production, does no validation other than a null check. /// Only handles non-repeated, non-extension fields. /// Also, doesn't handle enums or messages which need per-type validation. - void _$set(int index, value) { + void _$set(int index, Object? value) { assert(!_nonExtensionInfoByIndex(index).isRepeated); assert(_$check(index, value)); _ensureWritable(); @@ -538,14 +547,15 @@ _$check(index, value); // throw exception for null value } if (_hasObservers) { - _eventPlugin.beforeSetField(_nonExtensionInfoByIndex(index), value); + _eventPlugin!.beforeSetField(_nonExtensionInfoByIndex(index), value); } - var tag = _meta.byIndex[index].tagNumber; - var oneofIndex = _meta.oneofs[tag]; + final meta = _meta; + var tag = meta.byIndex[index].tagNumber; + var oneofIndex = meta.oneofs[tag]; if (oneofIndex != null) { - _clearField(_oneofCases[oneofIndex]); - _oneofCases[oneofIndex] = tag; + _clearField(_oneofCases![oneofIndex]); + _oneofCases![oneofIndex] = tag; } _values[index] = value; } @@ -560,24 +570,24 @@ void _clear() { _ensureWritable(); if (_unknownFields != null) { - _unknownFields.clear(); + _unknownFields!.clear(); } if (_hasObservers) { for (var fi in _infos) { - if (_values[fi.index] != null) { - _eventPlugin.beforeClearField(fi); + if (_values[fi.index!] != null) { + _eventPlugin!.beforeClearField(fi); } } if (_hasExtensions) { - for (var key in _extensions._tagNumbers) { - var fi = _extensions._getInfoOrNull(key); - _eventPlugin.beforeClearField(fi); + for (var key in _extensions!._tagNumbers) { + var fi = _extensions!._getInfoOrNull(key)!; + _eventPlugin!.beforeClearField(fi); } } } if (_values.isNotEmpty) _values.fillRange(0, _values.length, null); - if (_hasExtensions) _extensions._clearValues(); + if (_hasExtensions) _extensions!._clearValues(); } bool _equals(_FieldSet o) { @@ -586,20 +596,22 @@ if (!_equalFieldValues(_values[i], o._values[i])) return false; } - if (!_hasExtensions || !_extensions._hasValues) { + if (!_hasExtensions || !_extensions!._hasValues) { // Check if other extensions are logically empty. // (Don't create them unnecessarily.) - if (o._hasExtensions && o._extensions._hasValues) { + if (o._hasExtensions && o._extensions!._hasValues) { return false; } } else { - if (!_extensions._equalValues(o._extensions)) return false; + if (!_extensions!._equalValues(o._extensions)) return false; } - if (_unknownFields == null || _unknownFields.isEmpty) { + if (_unknownFields == null || _unknownFields!.isEmpty) { // Check if other unknown fields is logically empty. // (Don't create them unnecessarily.) - if (o._unknownFields != null && o._unknownFields.isNotEmpty) return false; + if (o._unknownFields != null && o._unknownFields!.isNotEmpty) { + return false; + } } else { // Check if the other unknown fields has the same fields. if (_unknownFields != o._unknownFields) return false; @@ -640,7 +652,7 @@ /// behavior, the hashCode can be memoized to speed up performance. int get _hashCode { if (_hashCodesCanBeMemoized && _memoizedHashCode != null) { - return _memoizedHashCode; + return _memoizedHashCode!; } // Hashes the value of one field (recursively). int hashField(int hash, FieldInfo fi, value) { @@ -667,15 +679,15 @@ int hashEachField(int hash) { //non-extension fields - hash = _infosSortedByTag.where((fi) => _values[fi.index] != null).fold( - hash, (int h, FieldInfo fi) => hashField(h, fi, _values[fi.index])); + hash = _infosSortedByTag.where((fi) => _values[fi.index!] != null).fold( + hash, (int h, FieldInfo fi) => hashField(h, fi, _values[fi.index!])); if (!_hasExtensions) return hash; hash = - _sorted(_extensions._tagNumbers).fold(hash, (int h, int tagNumber) { - var fi = _extensions._getInfoOrNull(tagNumber); - return hashField(h, fi, _extensions._getFieldOrNull(fi)); + _sorted(_extensions!._tagNumbers).fold(hash, (int h, int tagNumber) { + var fi = _extensions!._getInfoOrNull(tagNumber)!; + return hashField(h, fi, _extensions!._getFieldOrNull(fi)); }); return hash; @@ -728,15 +740,16 @@ } } - _infosSortedByTag - .forEach((FieldInfo fi) => writeFieldValue(_values[fi.index], fi.name)); + for (var fi in _infosSortedByTag) { + writeFieldValue(_values[fi.index!], fi.name); + } if (_hasExtensions) { - _extensions._info.keys.toList() + _extensions!._info.keys.toList() ..sort() ..forEach((int tagNumber) => writeFieldValue( - _extensions._values[tagNumber], - '[${_extensions._info[tagNumber].name}]')); + _extensions!._values[tagNumber], + '[${_extensions!._info[tagNumber]!.name}]')); } if (_hasUnknownFields) { out.write(_unknownFields.toString()); @@ -751,46 +764,47 @@ /// in this message. Repeated fields are appended. Singular sub-messages are /// recursively merged. void _mergeFromMessage(_FieldSet other) { - // TODO(https://github.com/dart-lang/protobuf/issues/60): Recognize - // when [this] and [other] are the same protobuf (e.g. from cloning). In + // TODO(https://github.com/google/protobuf.dart/issues/60): Recognize + // when `this` and [other] are the same protobuf (e.g. from cloning). In // this case, we can merge the non-extension fields without field lookups or // validation checks. for (var fi in other._infosSortedByTag) { - var value = other._values[fi.index]; + var value = other._values[fi.index!]; if (value != null) _mergeField(fi, value, isExtension: false); } if (other._hasExtensions) { - var others = other._extensions; + var others = other._extensions!; for (var tagNumber in others._tagNumbers) { - var extension = others._getInfoOrNull(tagNumber); + var extension = others._getInfoOrNull(tagNumber)!; var value = others._getFieldOrNull(extension); _mergeField(extension, value, isExtension: true); } } if (other._hasUnknownFields) { - _ensureUnknownFields().mergeFromUnknownFieldSet(other._unknownFields); + _ensureUnknownFields().mergeFromUnknownFieldSet(other._unknownFields!); } } - void _mergeField(FieldInfo otherFi, fieldValue, {bool isExtension}) { - var tagNumber = otherFi.tagNumber; + void _mergeField(FieldInfo otherFi, fieldValue, {bool? isExtension}) { + final tagNumber = otherFi.tagNumber; // Determine the FieldInfo to use. // Don't allow regular fields to be overwritten by extensions. - var fi = _nonExtensionInfo(tagNumber); - if (fi == null && isExtension) { + final meta = _meta; + var fi = _nonExtensionInfo(meta, tagNumber); + if (fi == null && isExtension!) { // This will overwrite any existing extension field info. fi = otherFi; } var mustClone = _isGroupOrMessage(otherFi.type); - if (fi.isMapField) { - MapFieldInfo f = fi; - mustClone = _isGroupOrMessage(f.valueFieldType); - PbMap map = f._ensureMapField(this); + if (fi!.isMapField) { + var f = fi as MapFieldInfo<dynamic, dynamic>; + mustClone = _isGroupOrMessage(f.valueFieldType!); + var map = f._ensureMapField(meta, this) as PbMap<dynamic, dynamic>; if (mustClone) { for (MapEntry entry in fieldValue.entries) { map[entry.key] = (entry.value as GeneratedMessage).deepCopy(); @@ -805,22 +819,22 @@ if (mustClone) { // fieldValue must be a PbListBase of GeneratedMessage. PbListBase<GeneratedMessage> pbList = fieldValue; - var repeatedFields = fi._ensureRepeatedField(this); + var repeatedFields = fi._ensureRepeatedField(meta, this); for (var i = 0; i < pbList.length; ++i) { repeatedFields.add(pbList[i].deepCopy()); } } else { // fieldValue must be at least a PbListBase. PbListBase pbList = fieldValue; - fi._ensureRepeatedField(this).addAll(pbList); + fi._ensureRepeatedField(meta, this).addAll(pbList); } return; } if (otherFi.isGroupOrMessage) { - final currentFi = isExtension - ? _ensureExtensions()._getFieldOrNull(fi) - : _values[fi.index]; + final currentFi = isExtension! + ? _ensureExtensions()._getFieldOrNull(fi as Extension<dynamic>) + : _values[fi.index!]; if (currentFi == null) { fieldValue = (fieldValue as GeneratedMessage).deepCopy(); @@ -829,11 +843,12 @@ } } - if (isExtension) { - _ensureExtensions()._setFieldAndInfo(fi, fieldValue); + if (isExtension!) { + _ensureExtensions() + ._setFieldAndInfo(fi as Extension<dynamic>, fieldValue); } else { _validateField(fi, fieldValue); - _setNonExtensionFieldUnchecked(fi, fieldValue); + _setNonExtensionFieldUnchecked(meta, fi, fieldValue); } } @@ -856,7 +871,7 @@ bool _hasRequiredValues() { if (!_hasRequiredFields) return true; for (var fi in _infos) { - var value = _values[fi.index]; + var value = _values[fi.index!]; if (!fi._hasRequiredValues(value)) return false; } return _hasRequiredExtensionValues(); @@ -864,8 +879,8 @@ bool _hasRequiredExtensionValues() { if (!_hasExtensions) return true; - for (var fi in _extensions._infos) { - var value = _extensions._getFieldOrNull(fi); + for (var fi in _extensions!._infos) { + var value = _extensions!._getFieldOrNull(fi); if (!fi._hasRequiredValues(value)) return false; } return true; // No problems found. @@ -875,11 +890,11 @@ void _appendInvalidFields(List<String> problems, String prefix) { if (!_hasRequiredFields) return; for (var fi in _infos) { - var value = _values[fi.index]; + var value = _values[fi.index!]; fi._appendInvalidFields(problems, value, prefix); } // TODO(skybrian): search extensions as well - // https://github.com/dart-lang/protobuf/issues/46 + // https://github.com/google/protobuf.dart/issues/46 } /// Makes a shallow copy of all values from [original] to this. @@ -887,31 +902,33 @@ /// Map fields and repeated fields are copied. void _shallowCopyValues(_FieldSet original) { _values.setRange(0, original._values.length, original._values); - for (var index = 0; index < _meta.byIndex.length; index++) { - var fieldInfo = _meta.byIndex[index]; + final info = _meta; + for (var index = 0; index < info.byIndex.length; index++) { + var fieldInfo = info.byIndex[index]; if (fieldInfo.isMapField) { - PbMap map = _values[index]; + PbMap? map = _values[index]; if (map != null) { - _values[index] = (fieldInfo as MapFieldInfo)._createMapField(_message) + _values[index] = (fieldInfo as MapFieldInfo) + ._createMapField(_message!) ..addAll(map); } } else if (fieldInfo.isRepeated) { - PbListBase list = _values[index]; + PbListBase? list = _values[index]; if (list != null) { - _values[index] = fieldInfo._createRepeatedField(_message) + _values[index] = fieldInfo._createRepeatedField(_message!) ..addAll(list); } } } if (original._hasExtensions) { - _ensureExtensions()._shallowCopyValues(original._extensions); + _ensureExtensions()._shallowCopyValues(original._extensions!); } if (original._hasUnknownFields) { - _ensureUnknownFields()._fields?.addAll(original._unknownFields._fields); + _ensureUnknownFields()._fields.addAll(original._unknownFields!._fields); } - _oneofCases?.addAll(original._oneofCases); + _oneofCases?.addAll(original._oneofCases!); } }
diff --git a/protobuf/lib/src/protobuf/field_type.dart b/protobuf/lib/src/protobuf/field_type.dart index 2052564..ae9f355 100644 --- a/protobuf/lib/src/protobuf/field_type.dart +++ b/protobuf/lib/src/protobuf/field_type.dart
@@ -2,6 +2,7 @@ // 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. +// ignore_for_file: constant_identifier_names,non_constant_identifier_names part of protobuf; bool _isRepeated(int fieldType) => (fieldType & PbFieldType._REPEATED_BIT) != 0; @@ -26,7 +27,7 @@ static int _baseType(int fieldType) => fieldType & ~(_REQUIRED_BIT | _REPEATED_BIT | _PACKED_BIT | _MAP_BIT); - static MakeDefaultFunc _defaultForType(int type) { + static MakeDefaultFunc? _defaultForType(int type) { switch (type) { case _OPTIONAL_BOOL: case _REQUIRED_BOOL:
diff --git a/protobuf/lib/src/protobuf/generated_message.dart b/protobuf/lib/src/protobuf/generated_message.dart index 0f1446a..60d40d1 100644 --- a/protobuf/lib/src/protobuf/generated_message.dart +++ b/protobuf/lib/src/protobuf/generated_message.dart
@@ -2,11 +2,13 @@ // 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. +// ignore_for_file: non_constant_identifier_names + part of protobuf; typedef CreateBuilderFunc = GeneratedMessage Function(); typedef MakeDefaultFunc = Function(); -typedef ValueOfFunc = ProtobufEnum Function(int value); +typedef ValueOfFunc = ProtobufEnum? Function(int value); /// The base class for all protobuf message types. /// @@ -17,23 +19,26 @@ /// GeneratedMessage_reservedNames and should be unlikely to be used in /// a proto file. abstract class GeneratedMessage { - _FieldSet _fieldSet; + _FieldSet? __fieldSet; + + @pragma('dart2js:tryInline') + _FieldSet get _fieldSet => __fieldSet!; GeneratedMessage() { - _fieldSet = _FieldSet(this, info_, eventPlugin); - if (eventPlugin != null) eventPlugin.attach(this); + __fieldSet = _FieldSet(this, info_, eventPlugin); + if (eventPlugin != null) eventPlugin!.attach(this); } GeneratedMessage.fromBuffer( List<int> input, ExtensionRegistry extensionRegistry) { - _fieldSet = _FieldSet(this, info_, eventPlugin); - if (eventPlugin != null) eventPlugin.attach(this); + __fieldSet = _FieldSet(this, info_, eventPlugin); + if (eventPlugin != null) eventPlugin!.attach(this); mergeFromBuffer(input, extensionRegistry); } GeneratedMessage.fromJson(String input, ExtensionRegistry extensionRegistry) { - _fieldSet = _FieldSet(this, info_, eventPlugin); - if (eventPlugin != null) eventPlugin.attach(this); + __fieldSet = _FieldSet(this, info_, eventPlugin); + if (eventPlugin != null) eventPlugin!.attach(this); mergeFromJson(input, extensionRegistry); } @@ -42,7 +47,7 @@ /// Subclasses can override this getter to be notified of changes /// to protobuf fields. - EventPlugin get eventPlugin => null; + EventPlugin? get eventPlugin => null; /// Creates a deep copy of the fields in this message. /// (The generated code uses [mergeFromMessage].) @@ -73,11 +78,11 @@ /// Returns a writable, shallow copy of this message. /// - /// Sub messages will be shared with [this] and will still be frozen if [this] + /// Sub messages will be shared with `this` and will still be frozen if `this` /// is frozen. /// /// The lists representing repeated fields are copied. But their elements will - /// be shared with the corresponding list in [this]. + /// be shared with the corresponding list in `this`. /// /// Similarly for map fields, the maps will be copied, but share the elements. // TODO(nichite, sigurdm): Consider returning an actual builder object that @@ -114,7 +119,7 @@ void clear() => _fieldSet._clear(); // TODO(antonm): move to getters. - int getTagNumber(String fieldName) => info_.tagNumber(fieldName); + int? getTagNumber(String fieldName) => info_.tagNumber(fieldName); @override bool operator ==(other) { @@ -171,8 +176,10 @@ _writeToCodedBufferWriter(_fieldSet, output); void mergeFromCodedBufferReader(CodedBufferReader input, - [ExtensionRegistry extensionRegistry = ExtensionRegistry.EMPTY]) => - _mergeFromCodedBufferReader(_fieldSet, input, extensionRegistry); + [ExtensionRegistry extensionRegistry = ExtensionRegistry.EMPTY]) { + final meta = _fieldSet._meta; + _mergeFromCodedBufferReader(meta, _fieldSet, input, extensionRegistry); + } /// Merges serialized protocol buffer data into this message. /// @@ -185,7 +192,8 @@ void mergeFromBuffer(List<int> input, [ExtensionRegistry extensionRegistry = ExtensionRegistry.EMPTY]) { var codedInput = CodedBufferReader(input); - _mergeFromCodedBufferReader(_fieldSet, codedInput, extensionRegistry); + final meta = _fieldSet._meta; + _mergeFromCodedBufferReader(meta, _fieldSet, codedInput, extensionRegistry); codedInput.checkLastTagWas(0); } @@ -208,10 +216,10 @@ /// actual runtime value) are represented as strings. Enumerated values are /// represented as their integer value. /// - /// For the proto3 JSON format use: [toProto3JSON]. + /// For the proto3 JSON format use: [toProto3Json]. String writeToJson() => jsonEncode(writeToJsonMap()); - /// Returns an Object representing Proto3 JSON serialization of [this]. + /// Returns an Object representing Proto3 JSON serialization of `this`. /// /// The key for each field is be the camel-cased name of the field. /// @@ -224,7 +232,7 @@ /// The [typeRegistry] is be used for encoding `Any` messages. If an `Any` /// message encoding a type not in [typeRegistry] is encountered, an /// error is thrown. - Object toProto3Json( + Object? toProto3Json( {TypeRegistry typeRegistry = const TypeRegistry.empty()}) => _writeToProto3Json(_fieldSet, typeRegistry); @@ -254,7 +262,7 @@ /// /// If the JSON is otherwise not formatted correctly (a String where a /// number was expected etc.) a [FormatException] is thrown. - void mergeFromProto3Json(Object json, + void mergeFromProto3Json(Object? json, {TypeRegistry typeRegistry = const TypeRegistry.empty(), bool ignoreUnknownFields = false, bool supportNamesWithUnderscores = true, @@ -265,7 +273,7 @@ /// Merges field values from [data], a JSON object, encoded as described by /// [GeneratedMessage.writeToJson]. /// - /// For the proto3 JSON format use: [mergeFromProto3JSON]. + /// For the proto3 JSON format use: [mergeFromProto3Json]. void mergeFromJson(String data, [ExtensionRegistry extensionRegistry = ExtensionRegistry.EMPTY]) { /// Disable lazy creation of Dart objects for a dart2js speedup. @@ -302,7 +310,7 @@ /// Clears an extension field and also removes the extension. void clearExtension(Extension extension) { if (_fieldSet._hasExtensions) { - _fieldSet._extensions._clearFieldAndInfo(extension); + _fieldSet._extensions!._clearFieldAndInfo(extension); } } @@ -312,7 +320,7 @@ /// The tagNumber should be a valid tag or extension. void clearField(int tagNumber) => _fieldSet._clearField(tagNumber); - int $_whichOneof(int oneofIndex) => _fieldSet._oneofCases[oneofIndex] ?? 0; + int $_whichOneof(int oneofIndex) => _fieldSet._oneofCases![oneofIndex] ?? 0; bool extensionsAreInitialized() => _fieldSet._hasRequiredExtensionValues(); @@ -334,13 +342,12 @@ /// validate all items added to it. This can most easily be done /// using the FieldInfo.check function. List<T> createRepeatedField<T>(int tagNumber, FieldInfo<T> fi) { - return PbList<T>(check: fi.check); + return PbList<T>(check: fi.check!); } /// Creates a Map representing a map field. Map<K, V> createMapField<K, V>(int tagNumber, MapFieldInfo<K, V> fi) { - return PbMap<K, V>( - fi.keyFieldType, fi.valueFieldType, fi.mapEntryBuilderInfo); + return PbMap<K, V>(fi.keyFieldType, fi.valueFieldType); } /// Returns the value of a field, ignoring any defaults. @@ -361,7 +368,7 @@ /// Returns [:true:] if a value of [extension] is present. bool hasExtension(Extension extension) => _fieldSet._hasExtensions && - _fieldSet._extensions._getFieldOrNull(extension) != null; + _fieldSet._extensions!._getFieldOrNull(extension) != null; /// Returns [:true:] if this message has a field associated with [tagNumber]. bool hasField(int tagNumber) => _fieldSet._hasField(tagNumber); @@ -371,6 +378,7 @@ /// Singular fields that are set in [other] overwrite the corresponding fields /// in this message. Repeated fields are appended. Singular sub-messages are /// recursively merged. + @pragma('dart2js:noInline') void mergeFromMessage(GeneratedMessage other) => _fieldSet._mergeFromMessage(other._fieldSet); @@ -379,8 +387,8 @@ .mergeFromUnknownFieldSet(unknownFieldSet); /// Sets the value of a non-repeated extension field to [value]. - void setExtension(Extension extension, value) { - if (value == null) throw ArgumentError('value is null'); + void setExtension(Extension extension, Object value) { + ArgumentError.checkNotNull(value, 'value'); if (_isRepeated(extension.type)) { throw ArgumentError(_fieldSet._setFieldFailedMessage( extension, value, 'repeating field (use get + .add())')); @@ -395,10 +403,9 @@ /// /// Throws an [:ArgumentError:] if [value] is [:null:]. To clear a field of /// it's current value, use [clearField] instead. - void setField(int tagNumber, value) { + @pragma('dart2js:noInline') + void setField(int tagNumber, Object value) { _fieldSet._setField(tagNumber, value); - return; // ignore: dead_code - return; // ignore: dead_code } /// For generated code only. @@ -426,7 +433,7 @@ List<T> $_getList<T>(int index) => _fieldSet._$getList<T>(index); /// For generated code only. - Map<K, V> $_getMap<K, V>(int index) => _fieldSet._$getMap<K, V>(index); + Map<K, V> $_getMap<K, V>(int index) => _fieldSet._$getMap<K, V>(this, index); /// For generated code only. bool $_getB(int index, bool defaultValue) => @@ -466,7 +473,8 @@ /// For generated code only. void $_setFloat(int index, double value) { - if (value == null || !_isFloat32(value)) { + ArgumentError.checkNotNull(value, 'value'); + if (!_isFloat32(value)) { _fieldSet._$check(index, value); } _fieldSet._$set(index, value); @@ -477,7 +485,8 @@ /// For generated code only. void $_setSignedInt32(int index, int value) { - if (value == null || !_isSigned32(value)) { + ArgumentError.checkNotNull(value, 'value'); + if (!_isSigned32(value)) { _fieldSet._$check(index, value); } _fieldSet._$set(index, value); @@ -485,7 +494,8 @@ /// For generated code only. void $_setUnsignedInt32(int index, int value) { - if (value == null || !_isUnsigned32(value)) { + ArgumentError.checkNotNull(value, 'value'); + if (!_isUnsigned32(value)) { _fieldSet._$check(index, value); } _fieldSet._$set(index, value); @@ -496,16 +506,17 @@ // Support for generating a read-only default singleton instance. - static final Map<Function, Function> _defaultMakers = {}; + static final Map<Function?, Function> _defaultMakers = {}; static T Function() _defaultMakerFor<T extends GeneratedMessage>( - T Function() createFn) { - return _defaultMakers[createFn] ??= _createDefaultMakerFor<T>(createFn); + T Function()? createFn) { + return (_defaultMakers[createFn] ??= _createDefaultMakerFor<T>(createFn!)) + as T Function(); } static T Function() _createDefaultMakerFor<T extends GeneratedMessage>( T Function() createFn) { - T defaultValue; + T? defaultValue; T defaultMaker() { return defaultValue ??= createFn()..freeze(); } @@ -537,10 +548,10 @@ throw ArgumentError('Rebuilding only works on frozen messages.'); } final t = toBuilder(); - updates(t); + updates(t as T); return t..freeze(); } /// Returns a writable deep copy of this message. - T deepCopy() => info_.createEmptyInstance()..mergeFromMessage(this); + T deepCopy() => info_.createEmptyInstance!() as T..mergeFromMessage(this); }
diff --git a/protobuf/lib/src/protobuf/json.dart b/protobuf/lib/src/protobuf/json.dart index c7da43d..c2c7368 100644 --- a/protobuf/lib/src/protobuf/json.dart +++ b/protobuf/lib/src/protobuf/json.dart
@@ -45,30 +45,31 @@ List _writeMap(dynamic fieldValue, MapFieldInfo fi) => List.from(fieldValue.entries.map((MapEntry e) => { - '${PbMap._keyFieldNumber}': convertToMap(e.key, fi.keyFieldType), + '${PbMap._keyFieldNumber}': convertToMap(e.key, fi.keyFieldType!), '${PbMap._valueFieldNumber}': - convertToMap(e.value, fi.valueFieldType) + convertToMap(e.value, fi.valueFieldType!) })); var result = <String, dynamic>{}; for (var fi in fs._infosSortedByTag) { - var value = fs._values[fi.index]; + var value = fs._values[fi.index!]; if (value == null || (value is List && value.isEmpty)) { continue; // It's missing, repeated, or an empty byte array. } if (_isMapField(fi.type)) { - result['${fi.tagNumber}'] = _writeMap(value, fi); + result['${fi.tagNumber}'] = + _writeMap(value, fi as MapFieldInfo<dynamic, dynamic>); continue; } result['${fi.tagNumber}'] = convertToMap(value, fi.type); } if (fs._hasExtensions) { - for (var tagNumber in _sorted(fs._extensions._tagNumbers)) { - var value = fs._extensions._values[tagNumber]; + for (var tagNumber in _sorted(fs._extensions!._tagNumbers)) { + var value = fs._extensions!._values[tagNumber]; if (value is List && value.isEmpty) { continue; // It's repeated or an empty byte array. } - var fi = fs._extensions._getInfoOrNull(tagNumber); + var fi = fs._extensions!._getInfoOrNull(tagNumber)!; result['$tagNumber'] = convertToMap(value, fi.type); } } @@ -78,9 +79,9 @@ // Merge fields from a previously decoded JSON object. // (Called recursively on nested messages.) void _mergeFromJsonMap( - _FieldSet fs, Map<String, dynamic> json, ExtensionRegistry registry) { - var keys = json.keys; - var meta = fs._meta; + _FieldSet fs, Map<String, dynamic> json, ExtensionRegistry? registry) { + final keys = json.keys; + final meta = fs._meta; for (var key in keys) { var fi = meta.byTagAsString[key]; if (fi == null) { @@ -89,18 +90,19 @@ if (fi == null) continue; // Unknown tag; skip } if (fi.isMapField) { - _appendJsonMap(fs, json[key], fi, registry); + _appendJsonMap( + meta, fs, json[key], fi as MapFieldInfo<dynamic, dynamic>, registry); } else if (fi.isRepeated) { - _appendJsonList(fs, json[key], fi, registry); + _appendJsonList(meta, fs, json[key], fi, registry); } else { - _setJsonField(fs, json[key], fi, registry); + _setJsonField(meta, fs, json[key], fi, registry); } } } -void _appendJsonList( - _FieldSet fs, List jsonList, FieldInfo fi, ExtensionRegistry registry) { - var repeated = fi._ensureRepeatedField(fs); +void _appendJsonList(BuilderInfo meta, _FieldSet fs, List jsonList, + FieldInfo fi, ExtensionRegistry? registry) { + final repeated = fi._ensureRepeatedField(meta, fs); // Micro optimization. Using "for in" generates the following and iterator // alloc: // for (t1 = J.get$iterator$ax(json), t2 = fi.tagNumber, t3 = fi.type, @@ -108,7 +110,7 @@ for (var i = 0, len = jsonList.length; i < len; i++) { var value = jsonList[i]; var convertedValue = - _convertJsonValue(fs, value, fi.tagNumber, fi.type, registry); + _convertJsonValue(meta, fs, value, fi.tagNumber, fi.type, registry); // In the case of an unknown enum value, the converted value may return // null. The default enum value should be used in these cases, which is // stored in the FieldInfo. @@ -117,22 +119,26 @@ } } -void _appendJsonMap( - _FieldSet fs, List jsonList, MapFieldInfo fi, ExtensionRegistry registry) { - PbMap map = fi._ensureMapField(fs); - for (Map<String, dynamic> jsonEntry in jsonList) { - var entryFieldSet = map._entryFieldSet(); - var convertedKey = _convertJsonValue( +void _appendJsonMap(BuilderInfo meta, _FieldSet fs, List jsonList, + MapFieldInfo fi, ExtensionRegistry? registry) { + final entryMeta = fi.mapEntryBuilderInfo; + final map = fi._ensureMapField(meta, fs) as PbMap<dynamic, dynamic>; + for (var jsonEntryDynamic in jsonList) { + var jsonEntry = jsonEntryDynamic as Map<String, dynamic>; + final entryFieldSet = _FieldSet(null, entryMeta, null); + final convertedKey = _convertJsonValue( + entryMeta, entryFieldSet, jsonEntry['${PbMap._keyFieldNumber}'], PbMap._keyFieldNumber, - fi.keyFieldType, + fi.keyFieldType!, registry); var convertedValue = _convertJsonValue( + entryMeta, entryFieldSet, jsonEntry['${PbMap._valueFieldNumber}'], PbMap._valueFieldNumber, - fi.valueFieldType, + fi.valueFieldType!, registry); // In the case of an unknown enum value, the converted value may return // null. The default enum value should be used in these cases, which is @@ -142,9 +148,10 @@ } } -void _setJsonField( - _FieldSet fs, json, FieldInfo fi, ExtensionRegistry registry) { - var value = _convertJsonValue(fs, json, fi.tagNumber, fi.type, registry); +void _setJsonField(BuilderInfo meta, _FieldSet fs, json, FieldInfo fi, + ExtensionRegistry? registry) { + final value = + _convertJsonValue(meta, fs, json, fi.tagNumber, fi.type, registry); if (value == null) return; // _convertJsonValue throws exception when it fails to do conversion. // Therefore we run _validateField for debug builds only to validate @@ -153,18 +160,18 @@ fs._validateField(fi, value); return true; }()); - fs._setFieldUnchecked(fi, value); + fs._setFieldUnchecked(meta, fi, value); } /// Converts [value] from the Json format to the Dart data type /// suitable for inserting into the corresponding [GeneratedMessage] field. /// -/// Returns the converted value. This function returns [null] if it is an +/// Returns the converted value. This function returns `null` if it is an /// unknown enum value, in which case the caller should figure out the default /// enum value to return instead. /// This function throws [ArgumentError] if it cannot convert the value. -dynamic _convertJsonValue(_FieldSet fs, value, int tagNumber, int fieldType, - ExtensionRegistry registry) { +dynamic _convertJsonValue(BuilderInfo meta, _FieldSet fs, value, int tagNumber, + int fieldType, ExtensionRegistry? registry) { String expectedType; // for exception message switch (PbFieldType._baseType(fieldType)) { case PbFieldType._BOOL_BIT: @@ -218,7 +225,7 @@ // The following call will return null if the enum value is unknown. // In that case, we want the caller to ignore this value, so we return // null from this method as well. - return fs._meta._decodeEnum(tagNumber, registry, value); + return meta._decodeEnum(tagNumber, registry, value); } expectedType = 'int or stringified int'; break; @@ -231,7 +238,7 @@ expectedType = 'int or stringified int'; break; case PbFieldType._FIXED32_BIT: - int validatedValue; + int? validatedValue; if (value is int) validatedValue = value; if (value is String) validatedValue = int.parse(value); if (validatedValue != null && validatedValue < 0) { @@ -252,8 +259,8 @@ case PbFieldType._GROUP_BIT: case PbFieldType._MESSAGE_BIT: if (value is Map) { - Map<String, dynamic> messageValue = value; - var subMessage = fs._meta._makeEmptyMessage(tagNumber, registry); + final messageValue = value as Map<String, dynamic>; + var subMessage = meta._makeEmptyMessage(tagNumber, registry); _mergeFromJsonMap(subMessage._fieldSet, messageValue, registry); return subMessage; }
diff --git a/protobuf/lib/src/protobuf/json_parsing_context.dart b/protobuf/lib/src/protobuf/json_parsing_context.dart index d9e9f53..5d11ced 100644 --- a/protobuf/lib/src/protobuf/json_parsing_context.dart +++ b/protobuf/lib/src/protobuf/json_parsing_context.dart
@@ -24,9 +24,9 @@ _path.removeLast(); } - /// Returns a FormatException indicating the indices to the current [path]. - Exception parseException(String message, Object source) { - var formattedPath = _path.map((s) => '[\"$s\"]').join(); + /// Returns a FormatException indicating the indices to the current path. + Exception parseException(String message, Object? source) { + var formattedPath = _path.map((s) => '["$s"]').join(); return FormatException( 'Protobuf JSON decoding failed at: root$formattedPath. $message', source);
diff --git a/protobuf/lib/src/protobuf/mixins/event_mixin.dart b/protobuf/lib/src/protobuf/mixins/event_mixin.dart index fa4ada0..985471a 100644 --- a/protobuf/lib/src/protobuf/mixins/event_mixin.dart +++ b/protobuf/lib/src/protobuf/mixins/event_mixin.dart
@@ -2,13 +2,10 @@ // 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. -library protobuf.mixins.event; - import 'dart:async' show StreamController, scheduleMicrotask; import 'dart:collection' show UnmodifiableListView; -import 'package:protobuf/protobuf.dart' - show GeneratedMessage, FieldInfo, EventPlugin; +import '../../../protobuf.dart' show GeneratedMessage, FieldInfo, EventPlugin; /// Provides a stream of changes to fields in a GeneratedMessage. /// (Experimental.) @@ -31,10 +28,10 @@ /// A change to a field in a GeneratedMessage. class PbFieldChange { - final GeneratedMessage message; + final GeneratedMessage? message; final FieldInfo info; - final oldValue; - final newValue; + final Object? oldValue; + final Object? newValue; PbFieldChange(this.message, this.info, this.oldValue, this.newValue); @@ -47,33 +44,33 @@ // An EventBuffer is created for each GeneratedMessage, so // initialization should be fast; create fields lazily. - GeneratedMessage _parent; - StreamController<List<PbFieldChange>> _controller; + GeneratedMessage? _parent; + StreamController<List<PbFieldChange>>? _controller; // If _buffer is non-null, at least one event is in the buffer // and a microtask has been scheduled to empty it. - List<PbFieldChange> _buffer; + List<PbFieldChange>? _buffer; @override - void attach(GeneratedMessage newParent) { + void attach(GeneratedMessage parent) { assert(_parent == null); - assert(newParent != null); - _parent = newParent; + ArgumentError.checkNotNull(parent, 'newParent'); + _parent = parent; } Stream<List<PbFieldChange>> get changes { - _controller ??= StreamController.broadcast(sync: true); - return _controller.stream; + var controller = _controller ??= StreamController.broadcast(sync: true); + return controller.stream; } @override - bool get hasObservers => _controller != null && _controller.hasListener; + bool get hasObservers => _controller != null && _controller!.hasListener; void deliverChanges() { var records = _buffer; _buffer = null; if (records != null && hasObservers) { - _controller.add(UnmodifiableListView<PbFieldChange>(records)); + _controller!.add(UnmodifiableListView<PbFieldChange>(records)); } } @@ -83,12 +80,12 @@ _buffer = <PbFieldChange>[]; scheduleMicrotask(deliverChanges); } - _buffer.add(change); + _buffer!.add(change); } @override void beforeSetField(FieldInfo fi, newValue) { - var oldValue = _parent.getFieldOrNull(fi.tagNumber); + var oldValue = _parent!.getFieldOrNull(fi.tagNumber); oldValue ??= fi.readonlyDefault; if (identical(oldValue, newValue)) return; addEvent(PbFieldChange(_parent, fi, oldValue, newValue)); @@ -96,7 +93,7 @@ @override void beforeClearField(FieldInfo fi) { - var oldValue = _parent.getFieldOrNull(fi.tagNumber); + var oldValue = _parent!.getFieldOrNull(fi.tagNumber); if (oldValue == null) return; var newValue = fi.readonlyDefault; addEvent(PbFieldChange(_parent, fi, oldValue, newValue));
diff --git a/protobuf/lib/src/protobuf/mixins/map_mixin.dart b/protobuf/lib/src/protobuf/mixins/map_mixin.dart index 2c32c71..0885719 100644 --- a/protobuf/lib/src/protobuf/mixins/map_mixin.dart +++ b/protobuf/lib/src/protobuf/mixins/map_mixin.dart
@@ -2,9 +2,7 @@ // 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. -library protobuf.mixins.map; - -import 'package:protobuf/protobuf.dart' show BuilderInfo; +import '../../../protobuf.dart' show BuilderInfo; /// Note that this class does not claim to implement [Map]. Instead, this needs /// to be specified using a dart_options.imports clause specifying MapMixin as a @@ -17,9 +15,9 @@ BuilderInfo get info_; void clear(); - int getTagNumber(String fieldName); + int? getTagNumber(String fieldName); dynamic getField(int tagNumber); - void setField(int tagNumber, var value); + void setField(int tagNumber, Object value); dynamic operator [](key) { if (key is! String) return null; @@ -39,7 +37,7 @@ Iterable<String> get keys => info_.byName.keys; - bool containsKey(Object key) => info_.byName.containsKey(key); + bool containsKey(Object? key) => info_.byName.containsKey(key); int get length => info_.byName.length;
diff --git a/protobuf/lib/src/protobuf/mixins/well_known.dart b/protobuf/lib/src/protobuf/mixins/well_known.dart index 496823f..e5215b2 100644 --- a/protobuf/lib/src/protobuf/mixins/well_known.dart +++ b/protobuf/lib/src/protobuf/mixins/well_known.dart
@@ -6,9 +6,8 @@ import 'package:fixnum/fixnum.dart'; -import '../json_parsing_context.dart'; import '../../../protobuf.dart'; -import '../type_registry.dart'; +import '../json_parsing_context.dart'; abstract class AnyMixin implements GeneratedMessage { String get typeUrl; @@ -85,7 +84,7 @@ throw ArgumentError( 'The type of the Any message (${any.typeUrl}) is not in the given typeRegistry.'); } - var unpacked = info.createEmptyInstance()..mergeFromBuffer(any.value); + var unpacked = info.createEmptyInstance!()..mergeFromBuffer(any.value); var proto3Json = unpacked.toProto3Json(); if (info.toProto3Json == null) { var map = proto3Json as Map<String, dynamic>; @@ -102,7 +101,7 @@ throw context.parseException( 'Expected Any message encoded as {@type,...},', json); } - final object = json as Map<String, dynamic>; + final object = json; final typeUrl = object['@type']; if (typeUrl is String) { @@ -114,12 +113,12 @@ json); } - Object subJson = info.fromProto3Json == null + Object? subJson = info.fromProto3Json == null // TODO(sigurdm): avoid cloning [object] here. ? (Map<String, dynamic>.from(object)..remove('@type')) : object['value']; // TODO(sigurdm): We lose [context.path]. - var packedMessage = info.createEmptyInstance() + var packedMessage = info.createEmptyInstance!() ..mergeFromProto3Json(subJson, typeRegistry: typeRegistry, supportNamesWithUnderscores: context.supportNamesWithUnderscores, @@ -156,7 +155,7 @@ seconds.toInt() * Duration.microsecondsPerSecond + nanos ~/ 1000, isUtc: true); - /// Updates [target] to be the time at [datetime]. + /// Updates [target] to be the time at [dateTime]. /// /// Time zone information will not be preserved. static void setFromDateTime(TimestampMixin target, DateTime dateTime) { @@ -231,9 +230,9 @@ if (json is String) { var jsonWithoutFracSec = json; var nanos = 0; - Match fracSecsMatch = RegExp(r'\.(\d+)').firstMatch(json); + Match? fracSecsMatch = RegExp(r'\.(\d+)').firstMatch(json); if (fracSecsMatch != null) { - var fracSecs = fracSecsMatch[1]; + var fracSecs = fracSecsMatch[1]!; if (fracSecs.length > 9) { throw context.parseException( 'Timestamp can have at most than 9 decimal digits', json); @@ -290,7 +289,7 @@ throw context.parseException( 'Expected a String of the form `<seconds>.<nanos>s`', json); } else { - var secondsString = match[1]; + var secondsString = match[1]!; var seconds = secondsString == '' ? Int64.ZERO : Int64.parseInt(secondsString); duration.seconds = seconds; @@ -326,13 +325,13 @@ var fields = (message as StructMixin).fields; var valueCreator = (message.info_.fieldInfo[_fieldsFieldTagNumber] as MapFieldInfo) - .valueCreator; + .valueCreator!; json.forEach((key, value) { if (key is! String) { throw context.parseException('Expected String key', json); } - ValueMixin v = valueCreator(); + var v = valueCreator() as ValueMixin; context.addMapIndex(key); ValueMixin.fromProto3JsonHelper(v, value, typeRegistry, context); context.popIndex(); @@ -368,7 +367,7 @@ // From google/protobuf/struct.proto: // The JSON representation for `Value` is JSON value - static Object toProto3JsonHelper( + static Object? toProto3JsonHelper( GeneratedMessage message, TypeRegistry typeRegistry) { var value = message as ValueMixin; // This would ideally be a switch, but we cannot import the enum we are @@ -390,7 +389,7 @@ } } - static void fromProto3JsonHelper(GeneratedMessage message, Object json, + static void fromProto3JsonHelper(GeneratedMessage message, Object? json, TypeRegistry typeRegistry, JsonParsingContext context) { var value = message as ValueMixin; if (json == null) { @@ -441,10 +440,10 @@ TypeRegistry typeRegistry, JsonParsingContext context) { var list = message as ListValueMixin; if (json is List) { - var subBuilder = message.info_.subBuilder(_valueFieldTagNumber); + var subBuilder = message.info_.subBuilder(_valueFieldTagNumber)!; for (var i = 0; i < json.length; i++) { Object element = json[i]; - ValueMixin v = subBuilder(); + var v = subBuilder() as ValueMixin; context.addListIndex(i); ValueMixin.fromProto3JsonHelper(v, element, typeRegistry, context); context.popIndex(); @@ -499,12 +498,12 @@ static String _toCamelCase(String name) { return name.replaceAllMapped( - RegExp('_([a-z])'), (Match m) => '${m.group(1).toUpperCase()}'); + RegExp('_([a-z])'), (Match m) => m.group(1)!.toUpperCase()); } static String _fromCamelCase(String name) { return name.replaceAllMapped( - RegExp('[A-Z]'), (Match m) => '_${m.group(0).toLowerCase()}'); + RegExp('[A-Z]'), (Match m) => '_${m.group(0)!.toLowerCase()}'); } }
diff --git a/protobuf/lib/src/protobuf/pb_list.dart b/protobuf/lib/src/protobuf/pb_list.dart index bc26f1b..6b53d1f 100644 --- a/protobuf/lib/src/protobuf/pb_list.dart +++ b/protobuf/lib/src/protobuf/pb_list.dart
@@ -4,7 +4,7 @@ part of protobuf; -typedef CheckFunc<E> = void Function(E x); +typedef CheckFunc<E> = void Function(E? x); class FrozenPbList<E> extends PbListBase<E> { FrozenPbList._(List<E> wrappedList) : super._(wrappedList); @@ -20,18 +20,18 @@ @override set length(int newLength) => throw _unsupported('set length'); @override - void setAll(int at, Iterable<E> iterable) => throw _unsupported('setAll'); + void setAll(int index, Iterable<E> iterable) => throw _unsupported('setAll'); @override - void add(E value) => throw _unsupported('add'); + void add(E? element) => throw _unsupported('add'); @override void addAll(Iterable<E> iterable) => throw _unsupported('addAll'); @override void insert(int index, E element) => throw _unsupported('insert'); @override - void insertAll(int at, Iterable<E> iterable) => + void insertAll(int index, Iterable<E> iterable) => throw _unsupported('insertAll'); @override - bool remove(Object element) => throw _unsupported('remove'); + bool remove(Object? element) => throw _unsupported('remove'); @override void removeWhere(bool Function(E element) test) => throw _unsupported('removeWhere'); @@ -39,9 +39,9 @@ void retainWhere(bool Function(E element) test) => throw _unsupported('retainWhere'); @override - void sort([Comparator<E> compare]) => throw _unsupported('sort'); + void sort([Comparator<E>? compare]) => throw _unsupported('sort'); @override - void shuffle([math.Random random]) => throw _unsupported('shuffle'); + void shuffle([math.Random? random]) => throw _unsupported('shuffle'); @override void clear() => throw _unsupported('clear'); @override @@ -55,15 +55,15 @@ @override void removeRange(int start, int end) => throw _unsupported('removeRange'); @override - void replaceRange(int start, int end, Iterable<E> iterable) => + void replaceRange(int start, int end, Iterable<E> newContents) => throw _unsupported('replaceRange'); @override - void fillRange(int start, int end, [E fillValue]) => + void fillRange(int start, int end, [E? fill]) => throw _unsupported('fillRange'); } class PbList<E> extends PbListBase<E> { - PbList({check = _checkNotNull}) : super._noList(check: check); + PbList({CheckFunc<E> check = _checkNotNull}) : super._noList(check: check); PbList.from(List from) : super._from(from); @@ -75,118 +75,86 @@ /// Freezes the list by converting to [FrozenPbList]. FrozenPbList<E> toFrozenPbList() => FrozenPbList<E>.from(this); - /// Adds [value] at the end of the list, extending the length by one. - /// Throws an [UnsupportedError] if the list is not extendable. @override - void add(E value) { - check(value); - _wrappedList.add(value); + void add(E element) { + check(element); + _wrappedList.add(element); } - /// Appends all elements of the [collection] to the end of list. - /// Extends the length of the list by the length of [collection]. - /// Throws an [UnsupportedError] if the list is not extendable. @override - void addAll(Iterable<E> collection) { - collection.forEach(check); - _wrappedList.addAll(collection); + void addAll(Iterable<E> iterable) { + iterable.forEach(check); + _wrappedList.addAll(iterable); } - /// Returns an [Iterable] of the objects in this list in reverse order. @override Iterable<E> get reversed => _wrappedList.reversed; - /// Sorts this list according to the order specified by the [compare] - /// function. @override - void sort([int Function(E a, E b) compare]) => _wrappedList.sort(compare); + void sort([int Function(E a, E b)? compare]) => _wrappedList.sort(compare); - /// Shuffles the elements of this list randomly. @override - void shuffle([math.Random random]) => _wrappedList.shuffle(random); + void shuffle([math.Random? random]) => _wrappedList.shuffle(random); - /// Removes all objects from this list; the length of the list becomes zero. @override void clear() => _wrappedList.clear(); - /// Inserts a new element in the list. - /// The element must be valid (and not nullable) for the PbList type. @override void insert(int index, E element) { check(element); _wrappedList.insert(index, element); } - /// Inserts all elements of [iterable] at position [index] in the list. - /// - /// Elements in [iterable] must be valid and not nullable for the PbList type. @override void insertAll(int index, Iterable<E> iterable) { iterable.forEach(check); _wrappedList.insertAll(index, iterable); } - /// Overwrites elements of `this` with elements of [iterable] starting at - /// position [index] in the list. - /// - /// Elements in [iterable] must be valid and not nullable for the PbList type. @override void setAll(int index, Iterable<E> iterable) { iterable.forEach(check); _wrappedList.setAll(index, iterable); } - /// Removes the first occurrence of [value] from this list. @override - bool remove(Object value) => _wrappedList.remove(value); + bool remove(Object? element) => _wrappedList.remove(element); - /// Removes the object at position [index] from this list. @override E removeAt(int index) => _wrappedList.removeAt(index); - /// Pops and returns the last object in this list. @override E removeLast() => _wrappedList.removeLast(); - /// Removes all objects from this list that satisfy [test]. @override void removeWhere(bool Function(E element) test) => _wrappedList.removeWhere(test); - /// Removes all objects from this list that fail to satisfy [test]. @override void retainWhere(bool Function(E element) test) => _wrappedList.retainWhere(test); - /// Copies [:end - start:] elements of the [from] array, starting from - /// [skipCount], into [:this:], starting at [start]. - /// Throws an [UnsupportedError] if the list is not extendable. @override - void setRange(int start, int end, Iterable<E> from, [int skipCount = 0]) { + void setRange(int start, int end, Iterable<E> iterable, [int skipCount = 0]) { // NOTE: In case `take()` returns less than `end - start` elements, the // _wrappedList will fail with a `StateError`. - from.skip(skipCount).take(end - start).forEach(check); - _wrappedList.setRange(start, end, from, skipCount); + iterable.skip(skipCount).take(end - start).forEach(check); + _wrappedList.setRange(start, end, iterable, skipCount); } - /// Removes the objects in the range [start] inclusive to [end] exclusive. @override void removeRange(int start, int end) => _wrappedList.removeRange(start, end); - /// Sets the objects in the range [start] inclusive to [end] exclusive to the - /// given [fillValue]. @override - void fillRange(int start, int end, [E fillValue]) { - check(fillValue); - _wrappedList.fillRange(start, end, fillValue); + void fillRange(int start, int end, [E? fill]) { + check(fill); + _wrappedList.fillRange(start, end, fill); } - /// Removes the objects in the range [start] inclusive to [end] exclusive and - /// inserts the contents of [replacement] in its place. @override - void replaceRange(int start, int end, Iterable<E> replacement) { - final values = replacement.toList(); - replacement.forEach(check); + void replaceRange(int start, int end, Iterable<E> newContents) { + final values = newContents.toList(); + newContents.forEach(check); _wrappedList.replaceRange(start, end, values); } } @@ -198,7 +166,7 @@ PbListBase._(this._wrappedList, {this.check = _checkNotNull}); PbListBase._noList({this.check = _checkNotNull}) : _wrappedList = <E>[] { - assert(check != null); + ArgumentError.checkNotNull(check, 'check'); } PbListBase._from(List from) @@ -213,167 +181,119 @@ @override int get hashCode => _HashUtils._hashObjects(_wrappedList); - /// Returns an [Iterator] for the list. @override Iterator<E> get iterator => _wrappedList.iterator; - /// Returns a new lazy [Iterable] with elements that are created by calling - /// `f` on each element of this `PbListBase` in iteration order. @override Iterable<T> map<T>(T Function(E e) f) => _wrappedList.map<T>(f); - /// Returns a new lazy [Iterable] with all elements that satisfy the predicate - /// [test]. @override Iterable<E> where(bool Function(E element) test) => _wrappedList.where(test); - /// Expands each element of this [Iterable] into zero or more elements. @override Iterable<T> expand<T>(Iterable<T> Function(E element) f) => _wrappedList.expand(f); - /// Returns true if the collection contains an element equal to [element]. @override - bool contains(Object element) => _wrappedList.contains(element); + bool contains(Object? element) => _wrappedList.contains(element); - /// Applies the function [f] to each element of this list in iteration order. @override - void forEach(void Function(E element) f) { - _wrappedList.forEach(f); + void forEach(void Function(E element) action) { + _wrappedList.forEach(action); } - /// Reduces a collection to a single value by iteratively combining elements - /// of the collection using the provided function. @override E reduce(E Function(E value, E element) combine) => _wrappedList.reduce(combine); - /// Reduces a collection to a single value by iteratively combining each - /// element of the collection with an existing value. @override T fold<T>(T initialValue, T Function(T previousValue, E element) combine) => _wrappedList.fold(initialValue, combine); - /// Checks whether every element of this iterable satisfies [test]. @override bool every(bool Function(E element) test) => _wrappedList.every(test); - /// Converts each element to a [String] and concatenates the strings. @override String join([String separator = '']) => _wrappedList.join(separator); - /// Checks whether any element of this iterable satisfies [test]. @override bool any(bool Function(E element) test) => _wrappedList.any(test); - /// Creates a [List] containing the elements of this [Iterable]. @override List<E> toList({bool growable = true}) => _wrappedList.toList(growable: growable); - /// Creates a [Set] containing the same elements as this iterable. @override Set<E> toSet() => _wrappedList.toSet(); - /// Returns `true` if there are no elements in this collection. @override bool get isEmpty => _wrappedList.isEmpty; - /// Returns `true` if there is at least one element in this collection. @override bool get isNotEmpty => _wrappedList.isNotEmpty; - /// Returns a lazy iterable of the [count] first elements of this iterable. @override Iterable<E> take(int count) => _wrappedList.take(count); - /// Returns a lazy iterable of the leading elements satisfying [test]. @override Iterable<E> takeWhile(bool Function(E value) test) => _wrappedList.takeWhile(test); - /// Returns an [Iterable] that provides all but the first [count] elements. @override Iterable<E> skip(int count) => _wrappedList.skip(count); - /// Returns an `Iterable` that skips leading elements while [test] is - /// satisfied. @override Iterable<E> skipWhile(bool Function(E value) test) => _wrappedList.skipWhile(test); - /// Returns the first element. @override E get first => _wrappedList.first; - /// Returns the last element. @override E get last => _wrappedList.last; - /// Checks that this iterable has only one element, and returns that element. @override E get single => _wrappedList.single; - /// Returns the first element that satisfies the given predicate [test]. @override - E firstWhere(bool Function(E element) test, {E Function() orElse}) => + E firstWhere(bool Function(E element) test, {E Function()? orElse}) => _wrappedList.firstWhere(test, orElse: orElse); - /// Returns the last element that satisfies the given predicate [test]. @override - E lastWhere(bool Function(E element) test, {E Function() orElse}) => + E lastWhere(bool Function(E element) test, {E Function()? orElse}) => _wrappedList.lastWhere(test, orElse: orElse); - /// Returns the single element that satisfies [test]. - // TODO(jakobr): Implement once Dart 2 corelib changes have landed. - //E singleWhere(bool test(E element), {E orElse()}) => - // _wrappedList.singleWhere(test, orElse: orElse); - - /// Returns the [index]th element. @override E elementAt(int index) => _wrappedList.elementAt(index); - /// Returns a string representation of (some of) the elements of `this`. @override String toString() => _wrappedList.toString(); - /// Returns the element at the given [index] in the list or throws an - /// [IndexOutOfRangeException] if [index] is out of bounds. @override E operator [](int index) => _wrappedList[index]; - /// Returns the number of elements in this collection. @override int get length => _wrappedList.length; // TODO(jakobr): E instead of Object once dart-lang/sdk#31311 is fixed. - /// Returns the first index of [element] in this list. @override - int indexOf(Object element, [int start = 0]) => - _wrappedList.indexOf(element, start); + int indexOf(Object? element, [int start = 0]) => + _wrappedList.indexOf(element as E, start); // TODO(jakobr): E instead of Object once dart-lang/sdk#31311 is fixed. - /// Returns the last index of [element] in this list. @override - int lastIndexOf(Object element, [int start]) => - _wrappedList.lastIndexOf(element, start); + int lastIndexOf(Object? element, [int? start]) => + _wrappedList.lastIndexOf(element as E, start); - /// Returns a new list containing the objects from [start] inclusive to [end] - /// exclusive. @override - List<E> sublist(int start, [int end]) => _wrappedList.sublist(start, end); + List<E> sublist(int start, [int? end]) => _wrappedList.sublist(start, end); - /// Returns an [Iterable] that iterates over the objects in the range [start] - /// inclusive to [end] exclusive. @override Iterable<E> getRange(int start, int end) => _wrappedList.getRange(start, end); - /// Returns an unmodifiable [Map] view of `this`. @override Map<int, E> asMap() => _wrappedList.asMap(); - /// Sets the entry at the given [index] in the list to [value]. - /// Throws an [IndexOutOfRangeException] if [index] is out of bounds. @override void operator []=(int index, E value) { check(value);
diff --git a/protobuf/lib/src/protobuf/pb_map.dart b/protobuf/lib/src/protobuf/pb_map.dart index a89713a..726ead6 100644 --- a/protobuf/lib/src/protobuf/pb_map.dart +++ b/protobuf/lib/src/protobuf/pb_map.dart
@@ -5,30 +5,28 @@ part of protobuf; class PbMap<K, V> extends MapBase<K, V> { - final int keyFieldType; - final int valueFieldType; + final int? keyFieldType; + final int? valueFieldType; static const int _keyFieldNumber = 1; static const int _valueFieldNumber = 2; final Map<K, V> _wrappedMap; - final BuilderInfo _entryBuilderInfo; bool _isReadonly = false; - _FieldSet _entryFieldSet() => _FieldSet(null, _entryBuilderInfo, null); - PbMap(this.keyFieldType, this.valueFieldType, this._entryBuilderInfo) + // The provided [info] will be ignored. + PbMap(this.keyFieldType, this.valueFieldType, [BuilderInfo? info]) : _wrappedMap = <K, V>{}; PbMap.unmodifiable(PbMap other) : keyFieldType = other.keyFieldType, valueFieldType = other.valueFieldType, _wrappedMap = Map.unmodifiable(other._wrappedMap), - _entryBuilderInfo = other._entryBuilderInfo, _isReadonly = other._isReadonly; @override - V operator [](Object key) => _wrappedMap[key]; + V? operator [](Object? key) => _wrappedMap[key]; @override void operator []=(K key, V value) { @@ -86,33 +84,30 @@ Iterable<K> get keys => _wrappedMap.keys; @override - V remove(Object key) { + V? remove(Object? key) { if (_isReadonly) { throw UnsupportedError('Attempted to change a read-only map field'); } return _wrappedMap.remove(key); } - @Deprecated('This function was not intended to be public. ' - 'It will be removed from the public api in next major version. ') - void add(CodedBufferReader input, [ExtensionRegistry registry]) { - _mergeEntry(input, registry); - } - - void _mergeEntry(CodedBufferReader input, [ExtensionRegistry registry]) { + void _mergeEntry(BuilderInfo mapEntryMeta, CodedBufferReader input, + [ExtensionRegistry? registry]) { var length = input.readInt32(); var oldLimit = input._currentLimit; input._currentLimit = input._bufferPos + length; - var entryFieldSet = _entryFieldSet(); - _mergeFromCodedBufferReader(entryFieldSet, input, registry); + final entryFieldSet = _FieldSet(null, mapEntryMeta, null); + _mergeFromCodedBufferReader(mapEntryMeta, entryFieldSet, input, registry!); input.checkLastTagWas(0); input._currentLimit = oldLimit; - var key = entryFieldSet._$get<K>(0, null); - var value = entryFieldSet._$get<V>(1, null); + var key = + entryFieldSet._values[0] ?? mapEntryMeta.byIndex[0].makeDefault!(); + var value = + entryFieldSet._values[1] ?? mapEntryMeta.byIndex[1].makeDefault!(); _wrappedMap[key] = value; } - void _checkNotNull(Object val) { + void _checkNotNull(Object? val) { if (val == null) { throw ArgumentError("Can't add a null to a map field"); } @@ -120,7 +115,7 @@ PbMap freeze() { _isReadonly = true; - if (_isGroupOrMessage(valueFieldType)) { + if (_isGroupOrMessage(valueFieldType!)) { for (var subMessage in values as Iterable<GeneratedMessage>) { subMessage.freeze(); }
diff --git a/protobuf/lib/src/protobuf/proto3_json.dart b/protobuf/lib/src/protobuf/proto3_json.dart index 852f0ed..6d35db9 100644 --- a/protobuf/lib/src/protobuf/proto3_json.dart +++ b/protobuf/lib/src/protobuf/proto3_json.dart
@@ -4,8 +4,8 @@ part of protobuf; -Object _writeToProto3Json(_FieldSet fs, TypeRegistry typeRegistry) { - String convertToMapKey(dynamic key, int keyType) { +Object? _writeToProto3Json(_FieldSet fs, TypeRegistry typeRegistry) { + String? convertToMapKey(dynamic key, int keyType) { var baseType = PbFieldType._baseType(keyType); assert(!_isRepeated(keyType)); @@ -32,10 +32,10 @@ } } - Object valueToProto3Json(dynamic fieldValue, int fieldType) { + Object? valueToProto3Json(dynamic fieldValue, int? fieldType) { if (fieldValue == null) return null; - if (_isGroupOrMessage(fieldType)) { + if (_isGroupOrMessage(fieldType!)) { return _writeToProto3Json( (fieldValue as GeneratedMessage)._fieldSet, typeRegistry); } else if (_isEnum(fieldType)) { @@ -81,13 +81,14 @@ } } - if (fs._meta.toProto3Json != null) { - return fs._meta.toProto3Json(fs._message, typeRegistry); + final meta = fs._meta; + if (meta.toProto3Json != null) { + return meta.toProto3Json!(fs._message!, typeRegistry); } var result = <String, dynamic>{}; for (var fieldInfo in fs._infosSortedByTag) { - var value = fs._values[fieldInfo.index]; + var value = fs._values[fieldInfo.index!]; if (value == null || (value is List && value.isEmpty)) { continue; // It's missing, repeated, or an empty byte array. } @@ -95,7 +96,7 @@ if (fieldInfo.isMapField) { jsonValue = (value as PbMap).map((key, entryValue) { var mapEntryInfo = fieldInfo as MapFieldInfo; - return MapEntry(convertToMapKey(key, mapEntryInfo.keyFieldType), + return MapEntry(convertToMapKey(key, mapEntryInfo.keyFieldType!), valueToProto3Json(entryValue, mapEntryInfo.valueFieldType)); }); } else if (fieldInfo.isRepeated) { @@ -111,8 +112,18 @@ return result; } +/// TODO(paulberry): find a better home for this? +extension _FindFirst<E> on Iterable<E> { + E? findFirst(bool Function(E) test) { + for (var element in this) { + if (test(element)) return element; + } + return null; + } +} + void _mergeFromProto3Json( - Object json, + Object? json, _FieldSet fieldSet, TypeRegistry typeRegistry, bool ignoreUnknownFields, @@ -121,7 +132,7 @@ var context = JsonParsingContext( ignoreUnknownFields, supportNamesWithUnderscores, permissiveEnums); - void recursionHelper(Object json, _FieldSet fieldSet) { + void recursionHelper(Object? json, _FieldSet fieldSet) { int tryParse32Bit(String s) { return int.tryParse(s) ?? (throw context.parseException('expected integer', s)); @@ -151,9 +162,9 @@ return result; } - Object convertProto3JsonValue(Object value, FieldInfo fieldInfo) { + Object? convertProto3JsonValue(Object? value, FieldInfo fieldInfo) { if (value == null) { - return fieldInfo.makeDefault(); + return fieldInfo.makeDefault!(); } var fieldType = fieldInfo.type; switch (PbFieldType._baseType(fieldType)) { @@ -197,15 +208,13 @@ if (value is String) { // TODO(sigurdm): Do we want to avoid linear search here? Measure... final result = permissiveEnums - ? fieldInfo.enumValues.firstWhere( - (e) => permissiveCompare(e.name, value), - orElse: () => null) - : fieldInfo.enumValues - .firstWhere((e) => e.name == value, orElse: () => null); + ? fieldInfo.enumValues! + .findFirst((e) => permissiveCompare(e.name, value)) + : fieldInfo.enumValues!.findFirst((e) => e.name == value); if ((result != null) || ignoreUnknownFields) return result; throw context.parseException('Unknown enum value', value); } else if (value is int) { - return fieldInfo.valueOf(value) ?? + return fieldInfo.valueOf!(value) ?? (ignoreUnknownFields ? null : (throw context.parseException( @@ -269,7 +278,7 @@ 'Expected int or stringified int', value); case PbFieldType._GROUP_BIT: case PbFieldType._MESSAGE_BIT: - var subMessage = fieldInfo.subBuilder(); + var subMessage = fieldInfo.subBuilder!(); recursionHelper(value, subMessage._fieldSet); return subMessage; default: @@ -319,16 +328,15 @@ return; } - var info = fieldSet._meta; - - final wellKnownConverter = info.fromProto3Json; + final meta = fieldSet._meta; + final wellKnownConverter = meta.fromProto3Json; if (wellKnownConverter != null) { - wellKnownConverter(fieldSet._message, json, typeRegistry, context); + wellKnownConverter(fieldSet._message!, json, typeRegistry, context); } else { if (json is Map) { - var byName = info.byName; + final byName = meta.byName; - json.forEach((key, value) { + json.forEach((key, Object? value) { if (key is! String) { throw context.parseException('Key was not a String', key); } @@ -338,9 +346,8 @@ if (fieldInfo == null && supportNamesWithUnderscores) { // We don't optimize for field names with underscores, instead do a // linear search for the index. - fieldInfo = byName.values.firstWhere( - (FieldInfo info) => info.protoName == key, - orElse: () => null); + fieldInfo = byName.values + .findFirst((FieldInfo info) => info.protoName == key); } if (fieldInfo == null) { if (ignoreUnknownFields) { @@ -352,19 +359,17 @@ if (_isMapField(fieldInfo.type)) { if (value is Map) { - MapFieldInfo mapFieldInfo = fieldInfo; - Map fieldValues = fieldSet._ensureMapField(fieldInfo); + final mapFieldInfo = fieldInfo as MapFieldInfo<dynamic, dynamic>; + final Map fieldValues = fieldSet._ensureMapField(meta, fieldInfo); value.forEach((subKey, subValue) { if (subKey is! String) { throw context.parseException('Expected a String key', subKey); } context.addMapIndex(subKey); - final result = fieldValues[ - decodeMapKey(subKey, mapFieldInfo.keyFieldType)] = + fieldValues[decodeMapKey(subKey, mapFieldInfo.keyFieldType!)] = convertProto3JsonValue( subValue, mapFieldInfo.valueFieldInfo); context.popIndex(); - return result; }); } else { throw context.parseException('Expected a map', value); @@ -372,9 +377,9 @@ } else if (_isRepeated(fieldInfo.type)) { if (value == null) { // `null` is accepted as the empty list []. - fieldSet._ensureRepeatedField(fieldInfo); + fieldSet._ensureRepeatedField(meta, fieldInfo); } else if (value is List) { - var values = fieldSet._ensureRepeatedField(fieldInfo); + var values = fieldSet._ensureRepeatedField(meta, fieldInfo); for (var i = 0; i < value.length; i++) { final entry = value[i]; context.addListIndex(i); @@ -385,19 +390,20 @@ throw context.parseException('Expected a list', value); } } else if (_isGroupOrMessage(fieldInfo.type)) { - // TODO(sigurdm) consider a cleaner separation between parsing and merging. - GeneratedMessage parsedSubMessage = - convertProto3JsonValue(value, fieldInfo); - GeneratedMessage original = fieldSet._values[fieldInfo.index]; + // TODO(sigurdm) consider a cleaner separation between parsing and + // merging. + var parsedSubMessage = + convertProto3JsonValue(value, fieldInfo) as GeneratedMessage; + GeneratedMessage? original = fieldSet._values[fieldInfo.index!]; if (original == null) { fieldSet._setNonExtensionFieldUnchecked( - fieldInfo, parsedSubMessage); + meta, fieldInfo, parsedSubMessage); } else { original.mergeFromMessage(parsedSubMessage); } } else { fieldSet._setFieldUnchecked( - fieldInfo, convertProto3JsonValue(value, fieldInfo)); + meta, fieldInfo, convertProto3JsonValue(value, fieldInfo)); } context.popIndex(); });
diff --git a/protobuf/lib/src/protobuf/protobuf_enum.dart b/protobuf/lib/src/protobuf/protobuf_enum.dart index 8b1b19d..32253db 100644 --- a/protobuf/lib/src/protobuf/protobuf_enum.dart +++ b/protobuf/lib/src/protobuf/protobuf_enum.dart
@@ -49,7 +49,7 @@ // instances, so `Object.operator==()` will work, and does not need to // be overridden explicitly. @override - bool operator ==(Object o); + bool operator ==(Object other); @override int get hashCode => value;
diff --git a/protobuf/lib/src/protobuf/readonly_message.dart b/protobuf/lib/src/protobuf/readonly_message.dart index 794f762..a5321e6 100644 --- a/protobuf/lib/src/protobuf/readonly_message.dart +++ b/protobuf/lib/src/protobuf/readonly_message.dart
@@ -17,7 +17,7 @@ void clearField(int tagNumber) => _readonly('clearField'); - List<T> createRepeatedField<T>(int tagNumber, FieldInfo<T> fi) { + List<T>? createRepeatedField<T>(int tagNumber, FieldInfo<T> fi) { _readonly('createRepeatedField'); return null; // not reached } @@ -47,7 +47,7 @@ void setExtension(Extension extension, var value) => _readonly('setExtension'); - void setField(int tagNumber, var value, [int fieldType]) => + void setField(int tagNumber, var value, [int? fieldType]) => _readonly('setField'); void _readonly(String methodName) {
diff --git a/protobuf/lib/src/protobuf/rpc_client.dart b/protobuf/lib/src/protobuf/rpc_client.dart index 94f7b4f..1b395ab 100644 --- a/protobuf/lib/src/protobuf/rpc_client.dart +++ b/protobuf/lib/src/protobuf/rpc_client.dart
@@ -7,7 +7,7 @@ /// Client side context. class ClientContext { /// The desired timeout of the RPC call. - final Duration timeout; + final Duration? timeout; ClientContext({this.timeout}); } @@ -28,7 +28,7 @@ /// appropriate. It should merge the reply into [emptyResponse] and /// return it. Future<T> invoke<T extends GeneratedMessage>( - ClientContext ctx, + ClientContext? ctx, String serviceName, String methodName, GeneratedMessage request,
diff --git a/protobuf/lib/src/protobuf/type_registry.dart b/protobuf/lib/src/protobuf/type_registry.dart index 8d73952..4485a7a 100644 --- a/protobuf/lib/src/protobuf/type_registry.dart +++ b/protobuf/lib/src/protobuf/type_registry.dart
@@ -26,7 +26,7 @@ const TypeRegistry.empty() : _mapping = const {}; - BuilderInfo lookup(String qualifiedName) { + BuilderInfo? lookup(String qualifiedName) { return _mapping[qualifiedName]; } }
diff --git a/protobuf/lib/src/protobuf/unknown_field_set.dart b/protobuf/lib/src/protobuf/unknown_field_set.dart index 3ec54cb..18a348f 100644 --- a/protobuf/lib/src/protobuf/unknown_field_set.dart +++ b/protobuf/lib/src/protobuf/unknown_field_set.dart
@@ -28,7 +28,7 @@ _fields.clear(); } - UnknownFieldSetField getField(int tagNumber) => _fields[tagNumber]; + UnknownFieldSetField? getField(int tagNumber) => _fields[tagNumber]; bool hasField(int tagNumber) => _fields.containsKey(tagNumber); @@ -88,7 +88,7 @@ void mergeFromUnknownFieldSet(UnknownFieldSet other) { _ensureWritable('mergeFromUnknownFieldSet'); for (var key in other._fields.keys) { - mergeField(key, other._fields[key]); + mergeField(key, other._fields[key]!); } } @@ -133,7 +133,7 @@ bool operator ==(other) { if (other is! UnknownFieldSet) return false; - UnknownFieldSet o = other; + var o = other; return _areMapsEqual(o._fields, _fields); } @@ -154,7 +154,7 @@ var stringBuffer = StringBuffer(); for (var tag in _sorted(_fields.keys)) { - var field = _fields[tag]; + var field = _fields[tag]!; for (var value in field.values) { if (value is UnknownFieldSet) { stringBuffer @@ -176,13 +176,15 @@ void writeToCodedBufferWriter(CodedBufferWriter output) { for (var key in _fields.keys) { - _fields[key].writeTo(key, output); + _fields[key]!.writeTo(key, output); } } void _markReadOnly() { if (_isReadOnly) return; - _fields.values.forEach((UnknownFieldSetField f) => f._markReadOnly()); + for (var f in _fields.values) { + f._markReadOnly(); + } _isReadOnly = true; } @@ -222,7 +224,7 @@ bool operator ==(other) { if (other is! UnknownFieldSetField) return false; - UnknownFieldSetField o = other; + var o = other; if (lengthDelimited.length != o.lengthDelimited.length) return false; for (var i = 0; i < lengthDelimited.length; i++) { if (!_areListsEqual(o.lengthDelimited[i], lengthDelimited[i])) { @@ -265,12 +267,13 @@ return hash; } - List get values => [] - ..addAll(lengthDelimited) - ..addAll(varints) - ..addAll(fixed32s) - ..addAll(fixed64s) - ..addAll(groups); + List get values => [ + ...lengthDelimited, + ...varints, + ...fixed32s, + ...fixed64s, + ...groups, + ]; void writeTo(int fieldNumber, CodedBufferWriter output) { void write(type, value) {
diff --git a/protobuf/lib/src/protobuf/wire_format.dart b/protobuf/lib/src/protobuf/wire_format.dart index 3b63eb8..e2ba932 100644 --- a/protobuf/lib/src/protobuf/wire_format.dart +++ b/protobuf/lib/src/protobuf/wire_format.dart
@@ -2,6 +2,8 @@ // 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. +// ignore_for_file: constant_identifier_names + part of protobuf; const int TAG_TYPE_BITS = 3;
diff --git a/protobuf/mono_pkg.yaml b/protobuf/mono_pkg.yaml index c3e79d2..5f8f170 100644 --- a/protobuf/mono_pkg.yaml +++ b/protobuf/mono_pkg.yaml
@@ -1,21 +1,16 @@ # See https://github.com/dart-lang/mono_repo for details -# - format stage runs only on Linux and only using dev SDK -# - analyze stage runs only on Linux with different flags for 2.10.5 and dev SDKs -# - test stage runs for all oses and all sdks - stages: - format_analyze: - group: - format - - analyze: --fatal-infos lib - - analyze: --fatal-infos test + - analyze: --fatal-infos dart: [dev] - group: - analyze: lib - analyze: test - dart: [2.10.5] + dart: [2.12.0] - run_tests: - group: [test] - dart: [2.10.5, dev] + dart: [2.12.0, dev] os: [linux, osx, windows]
diff --git a/protobuf/pubspec.yaml b/protobuf/pubspec.yaml index 50a253e..d57bb9b 100644 --- a/protobuf/pubspec.yaml +++ b/protobuf/pubspec.yaml
@@ -1,19 +1,20 @@ name: protobuf -version: 1.1.4 +version: 2.0.1 description: >- Runtime library for protocol buffers support. Use https://pub.dev/packages/protoc_plugin to generate dart code for your '.proto' files. -homepage: https://github.com/dart-lang/protobuf +homepage: https://github.com/google/protobuf.dart environment: - sdk: '>=2.7.0 <3.0.0' + sdk: '>=2.12.0 <3.0.0' dependencies: - fixnum: ^0.10.9 + fixnum: ^1.0.0 + collection: ^1.15.0 dev_dependencies: - test: '>=1.2.0' - benchmark_harness: any - js: any - matcher: any - pedantic: ^1.9.2 + test: ^1.16.0 + benchmark_harness: ^2.0.0 + js: ^0.6.3 + lints: ^1.0.0 + matcher: ^0.12.11
diff --git a/webview_flutter/BUILD.gn b/webview_flutter/BUILD.gn index 9d21fa4..f2985f5 100644 --- a/webview_flutter/BUILD.gn +++ b/webview_flutter/BUILD.gn
@@ -1,4 +1,4 @@ -# This file is generated by package_importer.py for webview_flutter-2.3.1 +# This file is generated by package_importer.py for webview_flutter-2.7.0 import("//build/dart/dart_library.gni")
diff --git a/webview_flutter/CHANGELOG.md b/webview_flutter/CHANGELOG.md index 57d32ef..1df7e53 100644 --- a/webview_flutter/CHANGELOG.md +++ b/webview_flutter/CHANGELOG.md
@@ -1,3 +1,23 @@ +## 2.7.0 + +* Adds `setCookie` to CookieManager. +* CreationParams now supports setting `initialCookies`. + +## 2.6.0 + +* Adds support for the `loadRequest` method. + +## 2.5.0 + +* Adds an option to set the background color of the webview. + +## 2.4.0 + +* Adds support for the `loadFile` and `loadHtmlString` methods. +* Updates example app Android compileSdkVersion to 31. +* Integration test fixes. +* Updates code for new analysis options. + ## 2.3.1 * Add iOS-specific note to set `JavascriptMode.unrestricted` in order to set `zoomEnabled: false`.
diff --git a/webview_flutter/README.md b/webview_flutter/README.md index de475ad..c3982ea 100644 --- a/webview_flutter/README.md +++ b/webview_flutter/README.md
@@ -92,3 +92,8 @@ To use Material Components when the user interacts with input elements in the WebView, follow the steps described in the [Enabling Material Components instructions](https://flutter.dev/docs/deployment/android#enabling-material-components). + +### Setting custom headers on POST requests + +Currently, setting custom headers when making a post request with the WebViewController's `loadRequest` method is not supported on Android. +If you require this functionality, a workaround is to make the request manually, and then load the response data using `loadHTMLString` instead. \ No newline at end of file
diff --git a/webview_flutter/example/android/app/build.gradle b/webview_flutter/example/android/app/build.gradle index 9a43699..532b2ad 100644 --- a/webview_flutter/example/android/app/build.gradle +++ b/webview_flutter/example/android/app/build.gradle
@@ -25,7 +25,7 @@ apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle" android { - compileSdkVersion 29 + compileSdkVersion 31 lintOptions { disable 'InvalidPackage' @@ -34,7 +34,7 @@ defaultConfig { // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). applicationId "io.flutter.plugins.webviewflutterexample" - minSdkVersion 19 + minSdkVersion 21 targetSdkVersion 28 versionCode flutterVersionCode.toInteger() versionName flutterVersionName
diff --git a/webview_flutter/example/integration_test/webview_flutter_test.dart b/webview_flutter/example/integration_test/webview_flutter_test.dart index 69ddcc6..cc74afd 100644 --- a/webview_flutter/example/integration_test/webview_flutter_test.dart +++ b/webview_flutter/example/integration_test/webview_flutter_test.dart
@@ -8,6 +8,7 @@ import 'dart:typed_data'; import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter/widgets.dart'; import 'package:flutter_test/flutter_test.dart'; @@ -180,91 +181,32 @@ }, skip: Platform.isAndroid && _skipDueToIssue86757); testWidgets('resize webview', (WidgetTester tester) async { - final String resizeTest = ''' - <!DOCTYPE html><html> - <head><title>Resize test</title> - <script type="text/javascript"> - function onResize() { - Resize.postMessage("resize"); - } - function onLoad() { - window.onresize = onResize; - } - </script> - </head> - <body onload="onLoad();" bgColor="blue"> - </body> - </html> - '''; - final String resizeTestBase64 = - base64Encode(const Utf8Encoder().convert(resizeTest)); - final Completer<void> resizeCompleter = Completer<void>(); - final Completer<void> pageStarted = Completer<void>(); - final Completer<void> pageLoaded = Completer<void>(); - final Completer<WebViewController> controllerCompleter = - Completer<WebViewController>(); - final GlobalKey key = GlobalKey(); + final Completer<void> initialResizeCompleter = Completer<void>(); + final Completer<void> buttonTapResizeCompleter = Completer<void>(); + final Completer<void> onPageFinished = Completer<void>(); - final WebView webView = WebView( - key: key, - initialUrl: 'data:text/html;charset=utf-8;base64,$resizeTestBase64', - onWebViewCreated: (WebViewController controller) { - controllerCompleter.complete(controller); + bool resizeButtonTapped = false; + await tester.pumpWidget(ResizableWebView( + onResize: (_) { + if (resizeButtonTapped) { + buttonTapResizeCompleter.complete(); + } else { + initialResizeCompleter.complete(); + } }, - javascriptChannels: <JavascriptChannel>{ - JavascriptChannel( - name: 'Resize', - onMessageReceived: (JavascriptMessage message) { - resizeCompleter.complete(true); - }, - ), - }, - onPageStarted: (String url) { - pageStarted.complete(null); - }, - onPageFinished: (String url) { - pageLoaded.complete(null); - }, - javascriptMode: JavascriptMode.unrestricted, + onPageFinished: () => onPageFinished.complete(), + )); + await onPageFinished.future; + // Wait for a potential call to resize after page is loaded. + await initialResizeCompleter.future.timeout( + const Duration(seconds: 3), + onTimeout: () => null, ); - await tester.pumpWidget( - Directionality( - textDirection: TextDirection.ltr, - child: Column( - children: <Widget>[ - SizedBox( - width: 200, - height: 200, - child: webView, - ), - ], - ), - ), - ); - - await controllerCompleter.future; - await pageStarted.future; - await pageLoaded.future; - - expect(resizeCompleter.isCompleted, false); - - await tester.pumpWidget( - Directionality( - textDirection: TextDirection.ltr, - child: Column( - children: <Widget>[ - SizedBox( - width: 400, - height: 400, - child: webView, - ), - ], - ), - ), - ); - - await resizeCompleter.future; + resizeButtonTapped = true; + await tester.tap(find.byKey(const ValueKey<String>('resizeButton'))); + await tester.pumpAndSettle(); + expect(buttonTapResizeCompleter.future, completes); }); testWidgets('set custom userAgent', (WidgetTester tester) async { @@ -518,10 +460,10 @@ testWidgets('Video plays inline when allowsInlineMediaPlayback is true', (WidgetTester tester) async { - Completer<WebViewController> controllerCompleter = + final Completer<WebViewController> controllerCompleter = Completer<WebViewController>(); - Completer<void> pageLoaded = Completer<void>(); - Completer<void> videoPlaying = Completer<void>(); + final Completer<void> pageLoaded = Completer<void>(); + final Completer<void> videoPlaying = Completer<void>(); await tester.pumpWidget( Directionality( @@ -552,7 +494,7 @@ ), ), ); - WebViewController controller = await controllerCompleter.future; + final WebViewController controller = await controllerCompleter.future; await pageLoaded.future; // Pump once to trigger the video play. @@ -561,7 +503,7 @@ // Makes sure we get the correct event that indicates the video is actually playing. await videoPlaying.future; - String fullScreen = + final String fullScreen = await controller.runJavascriptReturningResult('isFullScreen();'); expect(fullScreen, _webviewBool(false)); }); @@ -570,10 +512,10 @@ testWidgets( 'Video plays full screen when allowsInlineMediaPlayback is false', (WidgetTester tester) async { - Completer<WebViewController> controllerCompleter = + final Completer<WebViewController> controllerCompleter = Completer<WebViewController>(); - Completer<void> pageLoaded = Completer<void>(); - Completer<void> videoPlaying = Completer<void>(); + final Completer<void> pageLoaded = Completer<void>(); + final Completer<void> videoPlaying = Completer<void>(); await tester.pumpWidget( Directionality( @@ -604,7 +546,7 @@ ), ), ); - WebViewController controller = await controllerCompleter.future; + final WebViewController controller = await controllerCompleter.future; await pageLoaded.future; // Pump once to trigger the video play. @@ -613,7 +555,7 @@ // Makes sure we get the correct event that indicates the video is actually playing. await videoPlaying.future; - String fullScreen = + final String fullScreen = await controller.runJavascriptReturningResult('isFullScreen();'); expect(fullScreen, _webviewBool(true)); }, skip: Platform.isAndroid); @@ -791,7 +733,7 @@ }); testWidgets('getTitle', (WidgetTester tester) async { - final String getTitleTest = ''' + const String getTitleTest = ''' <!DOCTYPE html><html> <head><title>Some title</title> </head> @@ -811,6 +753,7 @@ textDirection: TextDirection.ltr, child: WebView( initialUrl: 'data:text/html;charset=utf-8;base64,$getTitleTestBase64', + javascriptMode: JavascriptMode.unrestricted, onWebViewCreated: (WebViewController controller) { controllerCompleter.complete(controller); }, @@ -828,6 +771,12 @@ await pageStarted.future; await pageLoaded.future; + // On at least iOS, it does not appear to be guaranteed that the native + // code has the title when the page load completes. Execute some JavaScript + // before checking the title to ensure that the page has been fully parsed + // and processed. + await controller.runJavascript('1;'); + final String? title = await controller.getTitle(); expect(title, 'Some title'); }); @@ -835,7 +784,7 @@ group('Programmatic Scroll', () { // TODO(bparrishMines): skipped due to https://github.com/flutter/flutter/issues/86757. testWidgets('setAndGetScrollPosition', (WidgetTester tester) async { - final String scrollTestPage = ''' + const String scrollTestPage = ''' <!DOCTYPE html> <html> <head> @@ -882,7 +831,7 @@ final WebViewController controller = await controllerCompleter.future; await pageLoaded.future; - await tester.pumpAndSettle(Duration(seconds: 3)); + await tester.pumpAndSettle(const Duration(seconds: 3)); int scrollPosX = await controller.getScrollX(); int scrollPosY = await controller.getScrollY(); @@ -922,7 +871,7 @@ // TODO(bparrishMines): skipped due to https://github.com/flutter/flutter/issues/86757. testWidgets('setAndGetScrollPosition', (WidgetTester tester) async { - final String scrollTestPage = ''' + const String scrollTestPage = ''' <!DOCTYPE html> <html> <head> @@ -969,7 +918,7 @@ final WebViewController controller = await controllerCompleter.future; await pageLoaded.future; - await tester.pumpAndSettle(Duration(seconds: 3)); + await tester.pumpAndSettle(const Duration(seconds: 3)); // Check scrollTo() const int X_SCROLL = 123; @@ -992,7 +941,7 @@ // TODO(bparrishMines): skipped due to https://github.com/flutter/flutter/issues/86757. testWidgets('inputs are scrolled into view when focused', (WidgetTester tester) async { - final String scrollTestPage = ''' + const String scrollTestPage = ''' <!DOCTYPE html> <html> <head> @@ -1044,7 +993,7 @@ ), ), ); - await Future.delayed(Duration(milliseconds: 20)); + await Future<dynamic>.delayed(const Duration(milliseconds: 20)); await tester.pump(); }); @@ -1053,7 +1002,7 @@ final String viewportRectJSON = await _runJavascriptReturningResult( controller, 'JSON.stringify(viewport.getBoundingClientRect())'); final Map<String, dynamic> viewportRectRelativeToViewport = - jsonDecode(viewportRectJSON); + jsonDecode(viewportRectJSON) as Map<String, dynamic>; // Check that the input is originally outside of the viewport. @@ -1061,7 +1010,7 @@ await _runJavascriptReturningResult( controller, 'JSON.stringify(inputEl.getBoundingClientRect())'); final Map<String, dynamic> initialInputClientRectRelativeToViewport = - jsonDecode(initialInputClientRectJSON); + jsonDecode(initialInputClientRectJSON) as Map<String, dynamic>; expect( initialInputClientRectRelativeToViewport['bottom'] <= @@ -1076,7 +1025,7 @@ await _runJavascriptReturningResult( controller, 'JSON.stringify(inputEl.getBoundingClientRect())'); final Map<String, dynamic> lastInputClientRectRelativeToViewport = - jsonDecode(lastInputClientRectJSON); + jsonDecode(lastInputClientRectJSON) as Map<String, dynamic>; expect( lastInputClientRectRelativeToViewport['top'] >= @@ -1099,7 +1048,7 @@ }); group('NavigationDelegate', () { - final String blankPage = "<!DOCTYPE html><head></head><body></body></html>"; + const String blankPage = '<!DOCTYPE html><head></head><body></body></html>'; final String blankPageEncoded = 'data:text/html;charset=utf-8;base64,' + base64Encode(const Utf8Encoder().convert(blankPage)); @@ -1195,7 +1144,7 @@ testWidgets( 'onWebResourceError only called for main frame', (WidgetTester tester) async { - final String iframeTest = ''' + const String iframeTest = ''' <!DOCTYPE html> <html> <head> @@ -1408,7 +1357,7 @@ testWidgets( 'JavaScript does not run in parent window', (WidgetTester tester) async { - final String iframe = ''' + const String iframe = ''' <!DOCTYPE html> <script> window.onload = () => { @@ -1494,5 +1443,78 @@ if (defaultTargetPlatform == TargetPlatform.iOS) { return await controller.runJavascriptReturningResult(js); } - return jsonDecode(await controller.runJavascriptReturningResult(js)); + return jsonDecode(await controller.runJavascriptReturningResult(js)) + as String; +} + +class ResizableWebView extends StatefulWidget { + const ResizableWebView( + {required this.onResize, required this.onPageFinished}); + + final JavascriptMessageHandler onResize; + final VoidCallback onPageFinished; + + @override + State<StatefulWidget> createState() => ResizableWebViewState(); +} + +class ResizableWebViewState extends State<ResizableWebView> { + double webViewWidth = 200; + double webViewHeight = 200; + + static const String resizePage = ''' + <!DOCTYPE html><html> + <head><title>Resize test</title> + <script type="text/javascript"> + function onResize() { + Resize.postMessage("resize"); + } + function onLoad() { + window.onresize = onResize; + } + </script> + </head> + <body onload="onLoad();" bgColor="blue"> + </body> + </html> + '''; + + @override + Widget build(BuildContext context) { + final String resizeTestBase64 = + base64Encode(const Utf8Encoder().convert(resizePage)); + return Directionality( + textDirection: TextDirection.ltr, + child: Column( + children: <Widget>[ + SizedBox( + width: webViewWidth, + height: webViewHeight, + child: WebView( + initialUrl: + 'data:text/html;charset=utf-8;base64,$resizeTestBase64', + javascriptChannels: <JavascriptChannel>{ + JavascriptChannel( + name: 'Resize', + onMessageReceived: widget.onResize, + ), + }, + onPageFinished: (_) => widget.onPageFinished(), + javascriptMode: JavascriptMode.unrestricted, + ), + ), + TextButton( + key: const Key('resizeButton'), + onPressed: () { + setState(() { + webViewWidth += 100.0; + webViewHeight += 100.0; + }); + }, + child: const Text('ResizeButton'), + ), + ], + ), + ); + } }
diff --git a/webview_flutter/example/lib/main.dart b/webview_flutter/example/lib/main.dart index b660ce3..65786de 100644 --- a/webview_flutter/example/lib/main.dart +++ b/webview_flutter/example/lib/main.dart
@@ -7,8 +7,10 @@ import 'dart:async'; import 'dart:convert'; import 'dart:io'; +import 'dart:typed_data'; import 'package:flutter/material.dart'; +import 'package:path_provider/path_provider.dart'; import 'package:webview_flutter/webview_flutter.dart'; void main() => runApp(MaterialApp(home: WebViewExample())); @@ -28,6 +30,46 @@ </html> '''; +const String kLocalExamplePage = ''' +<!DOCTYPE html> +<html lang="en"> +<head> +<title>Load file or HTML string example</title> +</head> +<body> + +<h1>Local demo page</h1> +<p> + This is an example page used to demonstrate how to load a local file or HTML + string using the <a href="https://pub.dev/packages/webview_flutter">Flutter + webview</a> plugin. +</p> + +</body> +</html> +'''; + +const String kTransparentBackgroundPage = ''' + <!DOCTYPE html> + <html> + <head> + <title>Transparent background test</title> + </head> + <style type="text/css"> + body { background: transparent; margin: 0; padding: 0; } + #container { position: relative; margin: 0; padding: 0; width: 100vw; height: 100vh; } + #shape { background: red; width: 200px; height: 200px; margin: 0; padding: 0; position: absolute; top: calc(50% - 100px); left: calc(50% - 100px); } + p { text-align: center; } + </style> + <body> + <div id="container"> + <p>Transparent background test</p> + <div id="shape"></div> + </div> + </body> + </html> +'''; + class WebViewExample extends StatefulWidget { @override _WebViewExampleState createState() => _WebViewExampleState(); @@ -40,12 +82,15 @@ @override void initState() { super.initState(); - if (Platform.isAndroid) WebView.platform = SurfaceAndroidWebView(); + if (Platform.isAndroid) { + WebView.platform = SurfaceAndroidWebView(); + } } @override Widget build(BuildContext context) { return Scaffold( + backgroundColor: Colors.green, appBar: AppBar( title: const Text('Flutter WebView example'), // This drop down menu demonstrates that Flutter widgets can be shown over the web view. @@ -64,7 +109,7 @@ _controller.complete(webViewController); }, onProgress: (int progress) { - print("WebView is loading (progress : $progress%)"); + print('WebView is loading (progress : $progress%)'); }, javascriptChannels: <JavascriptChannel>{ _toasterJavascriptChannel(context), @@ -84,6 +129,7 @@ print('Page finished loading: $url'); }, gestureNavigationEnabled: true, + backgroundColor: const Color(0x00000000), ); }), floatingActionButton: favoriteButton(), @@ -131,6 +177,11 @@ listCache, clearCache, navigationDelegate, + doPostRequest, + loadLocalFile, + loadHtmlString, + transparentBackground, + setCookie, } class SampleMenu extends StatelessWidget { @@ -146,6 +197,7 @@ builder: (BuildContext context, AsyncSnapshot<WebViewController> controller) { return PopupMenuButton<MenuOptions>( + key: const ValueKey<String>('ShowPopupMenu'), onSelected: (MenuOptions value) { switch (value) { case MenuOptions.showUserAgent: @@ -169,6 +221,21 @@ case MenuOptions.navigationDelegate: _onNavigationDelegateExample(controller.data!, context); break; + case MenuOptions.doPostRequest: + _onDoPostRequest(controller.data!, context); + break; + case MenuOptions.loadLocalFile: + _onLoadLocalFileExample(controller.data!, context); + break; + case MenuOptions.loadHtmlString: + _onLoadHtmlStringExample(controller.data!, context); + break; + case MenuOptions.transparentBackground: + _onTransparentBackground(controller.data!, context); + break; + case MenuOptions.setCookie: + _onSetCookie(controller.data!, context); + break; } }, itemBuilder: (BuildContext context) => <PopupMenuItem<MenuOptions>>[ @@ -201,13 +268,34 @@ value: MenuOptions.navigationDelegate, child: Text('Navigation Delegate example'), ), + const PopupMenuItem<MenuOptions>( + value: MenuOptions.doPostRequest, + child: Text('Post Request'), + ), + const PopupMenuItem<MenuOptions>( + value: MenuOptions.loadHtmlString, + child: Text('Load HTML string'), + ), + const PopupMenuItem<MenuOptions>( + value: MenuOptions.loadLocalFile, + child: Text('Load local file'), + ), + const PopupMenuItem<MenuOptions>( + key: ValueKey<String>('ShowTransparentBackgroundExample'), + value: MenuOptions.transparentBackground, + child: Text('Transparent background example'), + ), + const PopupMenuItem<MenuOptions>( + value: MenuOptions.setCookie, + child: Text('Set cookie'), + ), ], ); }, ); } - void _onShowUserAgent( + Future<void> _onShowUserAgent( WebViewController controller, BuildContext context) async { // Send a message with the user agent string to the Toaster JavaScript channel we registered // with the WebView. @@ -215,7 +303,7 @@ 'Toaster.postMessage("User Agent: " + navigator.userAgent);'); } - void _onListCookies( + Future<void> _onListCookies( WebViewController controller, BuildContext context) async { final String cookies = await controller.runJavascriptReturningResult('document.cookie'); @@ -232,7 +320,8 @@ )); } - void _onAddToCache(WebViewController controller, BuildContext context) async { + Future<void> _onAddToCache( + WebViewController controller, BuildContext context) async { await controller.runJavascript( 'caches.open("test_caches_entry"); localStorage["test_localStorage"] = "dummy_entry";'); // ignore: deprecated_member_use @@ -241,21 +330,23 @@ )); } - void _onListCache(WebViewController controller, BuildContext context) async { + Future<void> _onListCache( + WebViewController controller, BuildContext context) async { await controller.runJavascript('caches.keys()' '.then((cacheKeys) => JSON.stringify({"cacheKeys" : cacheKeys, "localStorage" : localStorage}))' '.then((caches) => Toaster.postMessage(caches))'); } - void _onClearCache(WebViewController controller, BuildContext context) async { + Future<void> _onClearCache( + WebViewController controller, BuildContext context) async { await controller.clearCache(); // ignore: deprecated_member_use Scaffold.of(context).showSnackBar(const SnackBar( - content: Text("Cache cleared."), + content: Text('Cache cleared.'), )); } - void _onClearCookies(BuildContext context) async { + Future<void> _onClearCookies(BuildContext context) async { final bool hadCookies = await cookieManager.clearCookies(); String message = 'There were cookies. Now, they are gone!'; if (!hadCookies) { @@ -267,13 +358,50 @@ )); } - void _onNavigationDelegateExample( + Future<void> _onNavigationDelegateExample( WebViewController controller, BuildContext context) async { final String contentBase64 = base64Encode(const Utf8Encoder().convert(kNavigationExamplePage)); await controller.loadUrl('data:text/html;base64,$contentBase64'); } + Future<void> _onSetCookie( + WebViewController controller, BuildContext context) async { + await CookieManager().setCookie( + const WebViewCookie( + name: 'foo', value: 'bar', domain: 'httpbin.org', path: '/anything'), + ); + await controller.loadUrl('https://httpbin.org/anything'); + } + + Future<void> _onDoPostRequest( + WebViewController controller, BuildContext context) async { + final WebViewRequest request = WebViewRequest( + uri: Uri.parse('https://httpbin.org/post'), + method: WebViewRequestMethod.post, + headers: <String, String>{'foo': 'bar', 'Content-Type': 'text/plain'}, + body: Uint8List.fromList('Test Body'.codeUnits), + ); + await controller.loadRequest(request); + } + + Future<void> _onLoadLocalFileExample( + WebViewController controller, BuildContext context) async { + final String pathToIndex = await _prepareLocalFile(); + + await controller.loadFile(pathToIndex); + } + + Future<void> _onLoadHtmlStringExample( + WebViewController controller, BuildContext context) async { + await controller.loadHtmlString(kLocalExamplePage); + } + + Future<void> _onTransparentBackground( + WebViewController controller, BuildContext context) async { + await controller.loadHtmlString(kTransparentBackgroundPage); + } + Widget _getCookieList(String cookies) { if (cookies == null || cookies == '""') { return Container(); @@ -287,6 +415,17 @@ children: cookieWidgets.toList(), ); } + + static Future<String> _prepareLocalFile() async { + final String tmpDir = (await getTemporaryDirectory()).path; + final File indexFile = File( + <String>{tmpDir, 'www', 'index.html'}.join(Platform.pathSeparator)); + + await indexFile.create(recursive: true); + await indexFile.writeAsString(kLocalExamplePage); + + return indexFile.path; + } } class NavigationControls extends StatelessWidget { @@ -316,7 +455,7 @@ } else { // ignore: deprecated_member_use Scaffold.of(context).showSnackBar( - const SnackBar(content: Text("No back history item")), + const SnackBar(content: Text('No back history item')), ); return; } @@ -333,7 +472,7 @@ // ignore: deprecated_member_use Scaffold.of(context).showSnackBar( const SnackBar( - content: Text("No forward history item")), + content: Text('No forward history item')), ); return; }
diff --git a/webview_flutter/example/pubspec.yaml b/webview_flutter/example/pubspec.yaml index 6b668eb..284a7a9 100644 --- a/webview_flutter/example/pubspec.yaml +++ b/webview_flutter/example/pubspec.yaml
@@ -9,6 +9,7 @@ dependencies: flutter: sdk: flutter + path_provider: ^2.0.6 webview_flutter: # When depending on this package from a real application you should use: # webview_flutter: ^x.y.z @@ -19,10 +20,10 @@ dev_dependencies: espresso: ^0.1.0+2 - flutter_test: - sdk: flutter flutter_driver: sdk: flutter + flutter_test: + sdk: flutter integration_test: sdk: flutter pedantic: ^1.10.0
diff --git a/webview_flutter/lib/platform_interface.dart b/webview_flutter/lib/platform_interface.dart index aa7b3a0..48f7434 100644 --- a/webview_flutter/lib/platform_interface.dart +++ b/webview_flutter/lib/platform_interface.dart
@@ -21,4 +21,7 @@ WebSetting, WebSettings, WebResourceError, - WebResourceErrorType; + WebResourceErrorType, + WebViewCookie, + WebViewRequest, + WebViewRequestMethod;
diff --git a/webview_flutter/lib/src/webview.dart b/webview_flutter/lib/src/webview.dart index 61702e5..8fe4f41 100644 --- a/webview_flutter/lib/src/webview.dart +++ b/webview_flutter/lib/src/webview.dart
@@ -3,19 +3,22 @@ // found in the LICENSE file. import 'dart:async'; +import 'dart:io'; import 'package:flutter/foundation.dart'; import 'package:flutter/gestures.dart'; import 'package:flutter/rendering.dart'; import 'package:flutter/widgets.dart'; import 'package:webview_flutter_android/webview_android.dart'; +import 'package:webview_flutter_android/webview_android_cookie_manager.dart'; +import 'package:webview_flutter_platform_interface/webview_flutter_platform_interface.dart'; import 'package:webview_flutter_wkwebview/webview_flutter_wkwebview.dart'; import '../platform_interface.dart'; /// Optional callback invoked when a web view is first created. [controller] is /// the [WebViewController] for the created web view. -typedef void WebViewCreatedCallback(WebViewController controller); +typedef WebViewCreatedCallback = void Function(WebViewController controller); /// Information about a navigation action that is about to be executed. class NavigationRequest { @@ -29,7 +32,7 @@ @override String toString() { - return '$runtimeType(url: $url, isForMainFrame: $isForMainFrame)'; + return 'NavigationRequest(url: $url, isForMainFrame: $isForMainFrame)'; } } @@ -48,20 +51,20 @@ /// `navigation` should be handled. /// /// See also: [WebView.navigationDelegate]. -typedef FutureOr<NavigationDecision> NavigationDelegate( +typedef NavigationDelegate = FutureOr<NavigationDecision> Function( NavigationRequest navigation); /// Signature for when a [WebView] has started loading a page. -typedef void PageStartedCallback(String url); +typedef PageStartedCallback = void Function(String url); /// Signature for when a [WebView] has finished loading a page. -typedef void PageFinishedCallback(String url); +typedef PageFinishedCallback = void Function(String url); /// Signature for when a [WebView] is loading a page. -typedef void PageLoadingCallback(int progress); +typedef PageLoadingCallback = void Function(int progress); /// Signature for when a [WebView] has failed to load a resource. -typedef void WebResourceErrorCallback(WebResourceError error); +typedef WebResourceErrorCallback = void Function(WebResourceError error); /// A web view widget for showing html content. /// @@ -79,6 +82,7 @@ Key? key, this.onWebViewCreated, this.initialUrl, + this.initialCookies = const <WebViewCookie>[], this.javascriptMode = JavascriptMode.disabled, this.javascriptChannels, this.navigationDelegate, @@ -94,6 +98,7 @@ this.initialMediaPlaybackPolicy = AutoMediaPlaybackPolicy.require_user_action_for_all_media_types, this.allowsInlineMediaPlayback = false, + this.backgroundColor, }) : assert(javascriptMode != null), assert(initialMediaPlaybackPolicy != null), assert(allowsInlineMediaPlayback != null), @@ -149,6 +154,9 @@ /// The initial URL to load. final String? initialUrl; + /// The initial cookies to set. + final List<WebViewCookie> initialCookies; + /// Whether JavaScript execution is enabled. final JavascriptMode javascriptMode; @@ -286,6 +294,12 @@ /// The default policy is [AutoMediaPlaybackPolicy.require_user_action_for_all_media_types]. final AutoMediaPlaybackPolicy initialMediaPlaybackPolicy; + /// The background color of the [WebView]. + /// + /// When `null` the platform's webview default background color is used. By + /// default [backgroundColor] is `null`. + final Color? backgroundColor; + @override State<StatefulWidget> createState() => _WebViewState(); } @@ -357,6 +371,8 @@ javascriptChannelNames: _extractChannelNames(widget.javascriptChannels), userAgent: widget.userAgent, autoMediaPlaybackPolicy: widget.initialMediaPlaybackPolicy, + backgroundColor: widget.backgroundColor, + cookies: widget.initialCookies, ); } @@ -391,7 +407,7 @@ bool? hasNavigationDelegate; bool? hasProgressTracking; bool? debuggingEnabled; - WebSetting<String?> userAgent = WebSetting.absent(); + WebSetting<String?> userAgent = const WebSetting<String?>.absent(); bool? zoomEnabled; if (currentValue.javascriptMode != newValue.javascriptMode) { javascriptMode = newValue.javascriptMode; @@ -468,6 +484,7 @@ } } + @override void onWebResourceError(WebResourceError error) { if (_widget.onWebResourceError != null) { _widget.onWebResourceError!(error); @@ -495,6 +512,35 @@ WebView _widget; + /// Loads the file located at the specified [absoluteFilePath]. + /// + /// The [absoluteFilePath] parameter should contain the absolute path to the + /// file as it is stored on the device. For example: + /// `/Users/username/Documents/www/index.html`. + /// + /// Throws an ArgumentError if the [absoluteFilePath] does not exist. + Future<void> loadFile( + String absoluteFilePath, + ) { + assert(absoluteFilePath.isNotEmpty); + return _webViewPlatformController.loadFile(absoluteFilePath); + } + + /// Loads the supplied HTML string. + /// + /// The [baseUrl] parameter is used when resolving relative URLs within the + /// HTML string. + Future<void> loadHtmlString( + String html, { + String? baseUrl, + }) { + assert(html.isNotEmpty); + return _webViewPlatformController.loadHtmlString( + html, + baseUrl: baseUrl, + ); + } + /// Loads the specified URL. /// /// If `headers` is not null and the URL is an HTTP URL, the key value paris in `headers` will @@ -512,6 +558,26 @@ return _webViewPlatformController.loadUrl(url, headers); } + /// Makes a specific HTTP request and loads the response in the webview. + /// + /// [WebViewRequest.method] must be one of the supported HTTP methods + /// in [WebViewRequestMethod]. + /// + /// If [WebViewRequest.headers] is not empty, its key-value pairs will be + /// added as the headers for the request. + /// + /// If [WebViewRequest.body] is not null, it will be added as the body + /// for the request. + /// + /// Throws an ArgumentError if [WebViewRequest.uri] has empty scheme. + /// + /// Android only: + /// When making a POST request, headers are ignored. As a workaround, make + /// the request manually and load the response data using [loadHTMLString]. + Future<void> loadRequest(WebViewRequest request) async { + return _webViewPlatformController.loadRequest(request); + } + /// Accessor to the current URL that the WebView is displaying. /// /// If [WebView.initialUrl] was never specified, returns `null`. @@ -721,16 +787,32 @@ return _instance ??= CookieManager._(); } - CookieManager._(); + CookieManager._() { + if (WebViewCookieManagerPlatform.instance == null) { + if (Platform.isAndroid) { + WebViewCookieManagerPlatform.instance = WebViewAndroidCookieManager(); + } else if (Platform.isIOS) { + WebViewCookieManagerPlatform.instance = WKWebViewCookieManager(); + } else { + throw AssertionError( + 'This platform is currently unsupported by webview_flutter.'); + } + } + } static CookieManager? _instance; /// Clears all cookies for all [WebView] instances. /// - /// This is a no op on iOS version smaller than 9. - /// /// Returns true if cookies were present before clearing, else false. - Future<bool> clearCookies() => WebView.platform.clearCookies(); + Future<bool> clearCookies() => + WebViewCookieManagerPlatform.instance!.clearCookies(); + + /// Sets a cookie for all [WebView] instances. + /// + /// This is a no op on iOS versions below 11. + Future<void> setCookie(WebViewCookie cookie) => + WebViewCookieManagerPlatform.instance!.setCookie(cookie); } // Throws an ArgumentError if `url` is not a valid URL string.
diff --git a/webview_flutter/pubspec.yaml b/webview_flutter/pubspec.yaml index b64e1b4..cd6c618 100644 --- a/webview_flutter/pubspec.yaml +++ b/webview_flutter/pubspec.yaml
@@ -2,7 +2,7 @@ description: A Flutter plugin that provides a WebView widget on Android and iOS. repository: https://github.com/flutter/plugins/tree/master/packages/webview_flutter/webview_flutter issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+webview%22 -version: 2.3.1 +version: 2.7.0 environment: sdk: ">=2.14.0 <3.0.0" @@ -19,13 +19,15 @@ dependencies: flutter: sdk: flutter - webview_flutter_android: ^2.2.0 - webview_flutter_platform_interface: ^1.2.0 - webview_flutter_wkwebview: ^2.2.0 + webview_flutter_android: ^2.8.0 + webview_flutter_platform_interface: ^1.8.0 + webview_flutter_wkwebview: ^2.6.0 dev_dependencies: + build_runner: ^2.1.5 flutter_driver: sdk: flutter flutter_test: sdk: flutter + mockito: ^5.0.16 pedantic: ^1.10.0
diff --git a/webview_flutter_android/BUILD.gn b/webview_flutter_android/BUILD.gn index 374db21..956d0df 100644 --- a/webview_flutter_android/BUILD.gn +++ b/webview_flutter_android/BUILD.gn
@@ -1,4 +1,4 @@ -# This file is generated by package_importer.py for webview_flutter_android-2.4.0 +# This file is generated by package_importer.py for webview_flutter_android-2.8.0 import("//build/dart/dart_library.gni") @@ -20,6 +20,7 @@ "src/android_webview_api_impls.dart", "src/instance_manager.dart", "webview_android.dart", + "webview_android_cookie_manager.dart", "webview_android_widget.dart", "webview_surface_android.dart", ]
diff --git a/webview_flutter_android/CHANGELOG.md b/webview_flutter_android/CHANGELOG.md index 479364f..7bd3387 100644 --- a/webview_flutter_android/CHANGELOG.md +++ b/webview_flutter_android/CHANGELOG.md
@@ -1,3 +1,19 @@ +## 2.8.0 + +* Implements new cookie manager for setting cookies and providing initial cookies. + +## 2.7.0 + +* Adds support for the `loadRequest` method from the platform interface. + +## 2.6.0 + +* Adds implementation of the `loadFlutterAsset` method from the platform interface. + +## 2.5.0 + +* Adds an option to set the background color of the webview. + ## 2.4.0 * Adds support for Android's `WebView.loadData` and `WebView.loadDataWithBaseUrl` methods and implements the `loadFile` and `loadHtmlString` methods from the platform interface. @@ -27,13 +43,12 @@ ## 2.0.15 -* Added Overrides in FlutterWebView.java - +* Added Overrides in FlutterWebView.java + ## 2.0.14 -* Update example App so navigation menu loads immediatly but only becomes available when `WebViewController` is available (same behavior as example App in webview_flutter package). +* Update example App so navigation menu loads immediatly but only becomes available when `WebViewController` is available (same behavior as example App in webview_flutter package). ## 2.0.13 * Extract Android implementation from `webview_flutter`. -
diff --git a/webview_flutter_android/android/build.gradle b/webview_flutter_android/android/build.gradle index e70d4e6..37954b3 100644 --- a/webview_flutter_android/android/build.gradle +++ b/webview_flutter_android/android/build.gradle
@@ -58,8 +58,4 @@ } } } - compileOptions { - sourceCompatibility JavaVersion.VERSION_1_8 - targetCompatibility JavaVersion.VERSION_1_8 - } }
diff --git a/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/CookieManagerHostApiImpl.java b/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/CookieManagerHostApiImpl.java new file mode 100644 index 0000000..3e38ce9 --- /dev/null +++ b/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/CookieManagerHostApiImpl.java
@@ -0,0 +1,29 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package io.flutter.plugins.webviewflutter; + +import android.os.Build; +import android.webkit.CookieManager; + +class CookieManagerHostApiImpl implements GeneratedAndroidWebView.CookieManagerHostApi { + @Override + public void clearCookies(GeneratedAndroidWebView.Result<Boolean> result) { + CookieManager cookieManager = CookieManager.getInstance(); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + cookieManager.removeAllCookies(result::success); + } else { + final boolean hasCookies = cookieManager.hasCookies(); + if (hasCookies) { + cookieManager.removeAllCookie(); + } + result.success(hasCookies); + } + } + + @Override + public void setCookie(String url, String value) { + CookieManager.getInstance().setCookie(url, value); + } +}
diff --git a/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/FlutterAssetManager.java b/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/FlutterAssetManager.java new file mode 100644 index 0000000..1d484d8 --- /dev/null +++ b/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/FlutterAssetManager.java
@@ -0,0 +1,108 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package io.flutter.plugins.webviewflutter; + +import android.content.res.AssetManager; +import androidx.annotation.NonNull; +import io.flutter.embedding.engine.plugins.FlutterPlugin; +import io.flutter.plugin.common.PluginRegistry; +import java.io.IOException; + +/** Provides access to the assets registered as part of the App bundle. */ +abstract class FlutterAssetManager { + final AssetManager assetManager; + + /** + * Constructs a new instance of the {@link FlutterAssetManager}. + * + * @param assetManager Instance of Android's {@link AssetManager} used to access assets within the + * App bundle. + */ + public FlutterAssetManager(AssetManager assetManager) { + this.assetManager = assetManager; + } + + /** + * Gets the relative file path to the Flutter asset with the given name, including the file's + * extension, e.g., "myImage.jpg". + * + * <p>The returned file path is relative to the Android app's standard asset's directory. + * Therefore, the returned path is appropriate to pass to Android's AssetManager, but the path is + * not appropriate to load as an absolute path. + */ + abstract String getAssetFilePathByName(String name); + + /** + * Returns a String array of all the assets at the given path. + * + * @param path A relative path within the assets, i.e., "docs/home.html". This value cannot be + * null. + * @return String[] Array of strings, one for each asset. These file names are relative to 'path'. + * This value may be null. + * @throws IOException Throws an IOException in case I/O operations were interrupted. + */ + public String[] list(@NonNull String path) throws IOException { + return assetManager.list(path); + } + + /** + * Provides access to assets using the {@link PluginRegistry.Registrar} for looking up file paths + * to Flutter assets. + * + * @deprecated The {@link RegistrarFlutterAssetManager} is for Flutter's v1 embedding. For + * instructions on migrating a plugin from Flutter's v1 Android embedding to v2, visit + * http://flutter.dev/go/android-plugin-migration + */ + @Deprecated + static class RegistrarFlutterAssetManager extends FlutterAssetManager { + final PluginRegistry.Registrar registrar; + + /** + * Constructs a new instance of the {@link RegistrarFlutterAssetManager}. + * + * @param assetManager Instance of Android's {@link AssetManager} used to access assets within + * the App bundle. + * @param registrar Instance of {@link io.flutter.plugin.common.PluginRegistry.Registrar} used + * to look up file paths to assets registered by Flutter. + */ + RegistrarFlutterAssetManager(AssetManager assetManager, PluginRegistry.Registrar registrar) { + super(assetManager); + this.registrar = registrar; + } + + @Override + public String getAssetFilePathByName(String name) { + return registrar.lookupKeyForAsset(name); + } + } + + /** + * Provides access to assets using the {@link FlutterPlugin.FlutterAssets} for looking up file + * paths to Flutter assets. + */ + static class PluginBindingFlutterAssetManager extends FlutterAssetManager { + final FlutterPlugin.FlutterAssets flutterAssets; + + /** + * Constructs a new instance of the {@link PluginBindingFlutterAssetManager}. + * + * @param assetManager Instance of Android's {@link AssetManager} used to access assets within + * the App bundle. + * @param flutterAssets Instance of {@link + * io.flutter.embedding.engine.plugins.FlutterPlugin.FlutterAssets} used to look up file + * paths to assets registered by Flutter. + */ + PluginBindingFlutterAssetManager( + AssetManager assetManager, FlutterPlugin.FlutterAssets flutterAssets) { + super(assetManager); + this.flutterAssets = flutterAssets; + } + + @Override + public String getAssetFilePathByName(String name) { + return flutterAssets.getAssetFilePathByName(name); + } + } +}
diff --git a/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/FlutterAssetManagerHostApiImpl.java b/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/FlutterAssetManagerHostApiImpl.java new file mode 100644 index 0000000..791912a --- /dev/null +++ b/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/FlutterAssetManagerHostApiImpl.java
@@ -0,0 +1,46 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package io.flutter.plugins.webviewflutter; + +import android.webkit.WebView; +import io.flutter.plugins.webviewflutter.GeneratedAndroidWebView.FlutterAssetManagerHostApi; +import java.io.IOException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +/** + * Host api implementation for {@link WebView}. + * + * <p>Handles creating {@link WebView}s that intercommunicate with a paired Dart object. + */ +public class FlutterAssetManagerHostApiImpl implements FlutterAssetManagerHostApi { + final FlutterAssetManager flutterAssetManager; + + /** Constructs a new instance of {@link FlutterAssetManagerHostApiImpl}. */ + public FlutterAssetManagerHostApiImpl(FlutterAssetManager flutterAssetManager) { + this.flutterAssetManager = flutterAssetManager; + } + + @Override + public List<String> list(String path) { + try { + String[] paths = flutterAssetManager.list(path); + + if (paths == null) { + return new ArrayList<>(); + } + + return Arrays.asList(paths); + } catch (IOException ex) { + throw new RuntimeException(ex.getMessage()); + } + } + + @Override + public String getAssetFilePathByName(String name) { + return flutterAssetManager.getAssetFilePathByName(name); + } +}
diff --git a/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/FlutterCookieManager.java b/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/FlutterCookieManager.java deleted file mode 100644 index df3f21d..0000000 --- a/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/FlutterCookieManager.java +++ /dev/null
@@ -1,56 +0,0 @@ -// Copyright 2013 The Flutter Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -package io.flutter.plugins.webviewflutter; - -import android.os.Build; -import android.os.Build.VERSION_CODES; -import android.webkit.CookieManager; -import android.webkit.ValueCallback; -import io.flutter.plugin.common.BinaryMessenger; -import io.flutter.plugin.common.MethodCall; -import io.flutter.plugin.common.MethodChannel; -import io.flutter.plugin.common.MethodChannel.MethodCallHandler; -import io.flutter.plugin.common.MethodChannel.Result; - -class FlutterCookieManager implements MethodCallHandler { - private final MethodChannel methodChannel; - - FlutterCookieManager(BinaryMessenger messenger) { - methodChannel = new MethodChannel(messenger, "plugins.flutter.io/cookie_manager"); - methodChannel.setMethodCallHandler(this); - } - - @Override - public void onMethodCall(MethodCall methodCall, Result result) { - switch (methodCall.method) { - case "clearCookies": - clearCookies(result); - break; - default: - result.notImplemented(); - } - } - - void dispose() { - methodChannel.setMethodCallHandler(null); - } - - private static void clearCookies(final Result result) { - CookieManager cookieManager = CookieManager.getInstance(); - final boolean hasCookies = cookieManager.hasCookies(); - if (Build.VERSION.SDK_INT >= VERSION_CODES.LOLLIPOP) { - cookieManager.removeAllCookies( - new ValueCallback<Boolean>() { - @Override - public void onReceiveValue(Boolean value) { - result.success(hasCookies); - } - }); - } else { - cookieManager.removeAllCookie(); - result.success(hasCookies); - } - } -}
diff --git a/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/GeneratedAndroidWebView.java b/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/GeneratedAndroidWebView.java index beb2d71..8ef0b8d 100644 --- a/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/GeneratedAndroidWebView.java +++ b/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/GeneratedAndroidWebView.java
@@ -16,6 +16,7 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.HashMap; +import java.util.List; import java.util.Map; /** Generated class from Pigeon. */ @@ -161,6 +162,94 @@ void error(Throwable error); } + private static class CookieManagerHostApiCodec extends StandardMessageCodec { + public static final CookieManagerHostApiCodec INSTANCE = new CookieManagerHostApiCodec(); + + private CookieManagerHostApiCodec() {} + } + + /** Generated interface from Pigeon that represents a handler of messages from Flutter. */ + public interface CookieManagerHostApi { + void clearCookies(Result<Boolean> result); + + void setCookie(String url, String value); + + /** The codec used by CookieManagerHostApi. */ + static MessageCodec<Object> getCodec() { + return CookieManagerHostApiCodec.INSTANCE; + } + + /** + * Sets up an instance of `CookieManagerHostApi` to handle messages through the + * `binaryMessenger`. + */ + static void setup(BinaryMessenger binaryMessenger, CookieManagerHostApi api) { + { + BasicMessageChannel<Object> channel = + new BasicMessageChannel<>( + binaryMessenger, + "dev.flutter.pigeon.CookieManagerHostApi.clearCookies", + getCodec()); + if (api != null) { + channel.setMessageHandler( + (message, reply) -> { + Map<String, Object> wrapped = new HashMap<>(); + try { + Result<Boolean> resultCallback = + new Result<Boolean>() { + public void success(Boolean result) { + wrapped.put("result", result); + reply.reply(wrapped); + } + + public void error(Throwable error) { + wrapped.put("error", wrapError(error)); + reply.reply(wrapped); + } + }; + + api.clearCookies(resultCallback); + } catch (Error | RuntimeException exception) { + wrapped.put("error", wrapError(exception)); + reply.reply(wrapped); + } + }); + } else { + channel.setMessageHandler(null); + } + } + { + BasicMessageChannel<Object> channel = + new BasicMessageChannel<>( + binaryMessenger, "dev.flutter.pigeon.CookieManagerHostApi.setCookie", getCodec()); + if (api != null) { + channel.setMessageHandler( + (message, reply) -> { + Map<String, Object> wrapped = new HashMap<>(); + try { + ArrayList<Object> args = (ArrayList<Object>) message; + String urlArg = (String) args.get(0); + if (urlArg == null) { + throw new NullPointerException("urlArg unexpectedly null."); + } + String valueArg = (String) args.get(1); + if (valueArg == null) { + throw new NullPointerException("valueArg unexpectedly null."); + } + api.setCookie(urlArg, valueArg); + wrapped.put("result", null); + } catch (Error | RuntimeException exception) { + wrapped.put("error", wrapError(exception)); + } + reply.reply(wrapped); + }); + } else { + channel.setMessageHandler(null); + } + } + } + } + private static class WebViewHostApiCodec extends StandardMessageCodec { public static final WebViewHostApiCodec INSTANCE = new WebViewHostApiCodec(); @@ -185,6 +274,8 @@ void loadUrl(Long instanceId, String url, Map<String, String> headers); + void postUrl(Long instanceId, String url, byte[] data); + String getUrl(Long instanceId); Boolean canGoBack(Long instanceId); @@ -223,6 +314,8 @@ void setWebChromeClient(Long instanceId, Long clientInstanceId); + void setBackgroundColor(Long instanceId, Long color); + /** The codec used by WebViewHostApi. */ static MessageCodec<Object> getCodec() { return WebViewHostApiCodec.INSTANCE; @@ -410,6 +503,39 @@ { BasicMessageChannel<Object> channel = new BasicMessageChannel<>( + binaryMessenger, "dev.flutter.pigeon.WebViewHostApi.postUrl", getCodec()); + if (api != null) { + channel.setMessageHandler( + (message, reply) -> { + Map<String, Object> wrapped = new HashMap<>(); + try { + ArrayList<Object> args = (ArrayList<Object>) message; + Number instanceIdArg = (Number) args.get(0); + if (instanceIdArg == null) { + throw new NullPointerException("instanceIdArg unexpectedly null."); + } + String urlArg = (String) args.get(1); + if (urlArg == null) { + throw new NullPointerException("urlArg unexpectedly null."); + } + byte[] dataArg = (byte[]) args.get(2); + if (dataArg == null) { + throw new NullPointerException("dataArg unexpectedly null."); + } + api.postUrl(instanceIdArg.longValue(), urlArg, dataArg); + wrapped.put("result", null); + } catch (Error | RuntimeException exception) { + wrapped.put("error", wrapError(exception)); + } + reply.reply(wrapped); + }); + } else { + channel.setMessageHandler(null); + } + } + { + BasicMessageChannel<Object> channel = + new BasicMessageChannel<>( binaryMessenger, "dev.flutter.pigeon.WebViewHostApi.getUrl", getCodec()); if (api != null) { channel.setMessageHandler( @@ -958,6 +1084,37 @@ channel.setMessageHandler(null); } } + { + BasicMessageChannel<Object> channel = + new BasicMessageChannel<>( + binaryMessenger, + "dev.flutter.pigeon.WebViewHostApi.setBackgroundColor", + getCodec()); + if (api != null) { + channel.setMessageHandler( + (message, reply) -> { + Map<String, Object> wrapped = new HashMap<>(); + try { + ArrayList<Object> args = (ArrayList<Object>) message; + Number instanceIdArg = (Number) args.get(0); + if (instanceIdArg == null) { + throw new NullPointerException("instanceIdArg unexpectedly null."); + } + Number colorArg = (Number) args.get(1); + if (colorArg == null) { + throw new NullPointerException("colorArg unexpectedly null."); + } + api.setBackgroundColor(instanceIdArg.longValue(), colorArg.longValue()); + wrapped.put("result", null); + } catch (Error | RuntimeException exception) { + wrapped.put("error", wrapError(exception)); + } + reply.reply(wrapped); + }); + } else { + channel.setMessageHandler(null); + } + } } } @@ -1882,6 +2039,84 @@ } } + private static class FlutterAssetManagerHostApiCodec extends StandardMessageCodec { + public static final FlutterAssetManagerHostApiCodec INSTANCE = + new FlutterAssetManagerHostApiCodec(); + + private FlutterAssetManagerHostApiCodec() {} + } + + /** Generated interface from Pigeon that represents a handler of messages from Flutter. */ + public interface FlutterAssetManagerHostApi { + List<String> list(String path); + + String getAssetFilePathByName(String name); + + /** The codec used by FlutterAssetManagerHostApi. */ + static MessageCodec<Object> getCodec() { + return FlutterAssetManagerHostApiCodec.INSTANCE; + } + + /** + * Sets up an instance of `FlutterAssetManagerHostApi` to handle messages through the + * `binaryMessenger`. + */ + static void setup(BinaryMessenger binaryMessenger, FlutterAssetManagerHostApi api) { + { + BasicMessageChannel<Object> channel = + new BasicMessageChannel<>( + binaryMessenger, "dev.flutter.pigeon.FlutterAssetManagerHostApi.list", getCodec()); + if (api != null) { + channel.setMessageHandler( + (message, reply) -> { + Map<String, Object> wrapped = new HashMap<>(); + try { + ArrayList<Object> args = (ArrayList<Object>) message; + String pathArg = (String) args.get(0); + if (pathArg == null) { + throw new NullPointerException("pathArg unexpectedly null."); + } + List<String> output = api.list(pathArg); + wrapped.put("result", output); + } catch (Error | RuntimeException exception) { + wrapped.put("error", wrapError(exception)); + } + reply.reply(wrapped); + }); + } else { + channel.setMessageHandler(null); + } + } + { + BasicMessageChannel<Object> channel = + new BasicMessageChannel<>( + binaryMessenger, + "dev.flutter.pigeon.FlutterAssetManagerHostApi.getAssetFilePathByName", + getCodec()); + if (api != null) { + channel.setMessageHandler( + (message, reply) -> { + Map<String, Object> wrapped = new HashMap<>(); + try { + ArrayList<Object> args = (ArrayList<Object>) message; + String nameArg = (String) args.get(0); + if (nameArg == null) { + throw new NullPointerException("nameArg unexpectedly null."); + } + String output = api.getAssetFilePathByName(nameArg); + wrapped.put("result", output); + } catch (Error | RuntimeException exception) { + wrapped.put("error", wrapError(exception)); + } + reply.reply(wrapped); + }); + } else { + channel.setMessageHandler(null); + } + } + } + } + private static class WebChromeClientFlutterApiCodec extends StandardMessageCodec { public static final WebChromeClientFlutterApiCodec INSTANCE = new WebChromeClientFlutterApiCodec();
diff --git a/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/WebViewFlutterPlugin.java b/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/WebViewFlutterPlugin.java index 2b174ff..4ef622f 100644 --- a/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/WebViewFlutterPlugin.java +++ b/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/WebViewFlutterPlugin.java
@@ -13,7 +13,9 @@ import io.flutter.embedding.engine.plugins.activity.ActivityPluginBinding; import io.flutter.plugin.common.BinaryMessenger; import io.flutter.plugin.platform.PlatformViewRegistry; +import io.flutter.plugins.webviewflutter.GeneratedAndroidWebView.CookieManagerHostApi; import io.flutter.plugins.webviewflutter.GeneratedAndroidWebView.DownloadListenerHostApi; +import io.flutter.plugins.webviewflutter.GeneratedAndroidWebView.FlutterAssetManagerHostApi; import io.flutter.plugins.webviewflutter.GeneratedAndroidWebView.JavaScriptChannelHostApi; import io.flutter.plugins.webviewflutter.GeneratedAndroidWebView.WebChromeClientHostApi; import io.flutter.plugins.webviewflutter.GeneratedAndroidWebView.WebSettingsHostApi; @@ -29,7 +31,6 @@ */ public class WebViewFlutterPlugin implements FlutterPlugin, ActivityAware { private FlutterPluginBinding pluginBinding; - private FlutterCookieManager flutterCookieManager; private WebViewHostApiImpl webViewHostApi; private JavaScriptChannelHostApiImpl javaScriptChannelHostApi; @@ -61,16 +62,17 @@ registrar.messenger(), registrar.platformViewRegistry(), registrar.activity(), - registrar.view()); - new FlutterCookieManager(registrar.messenger()); + registrar.view(), + new FlutterAssetManager.RegistrarFlutterAssetManager( + registrar.context().getAssets(), registrar)); } private void setUp( BinaryMessenger binaryMessenger, PlatformViewRegistry viewRegistry, Context context, - View containerView) { - new FlutterCookieManager(binaryMessenger); + View containerView, + FlutterAssetManager flutterAssetManager) { InstanceManager instanceManager = new InstanceManager(); @@ -111,6 +113,9 @@ binaryMessenger, new WebSettingsHostApiImpl( instanceManager, new WebSettingsHostApiImpl.WebSettingsCreator())); + FlutterAssetManagerHostApi.setup( + binaryMessenger, new FlutterAssetManagerHostApiImpl(flutterAssetManager)); + CookieManagerHostApi.setup(binaryMessenger, new CookieManagerHostApiImpl()); } @Override @@ -120,18 +125,13 @@ binding.getBinaryMessenger(), binding.getPlatformViewRegistry(), binding.getApplicationContext(), - null); + null, + new FlutterAssetManager.PluginBindingFlutterAssetManager( + binding.getApplicationContext().getAssets(), binding.getFlutterAssets())); } @Override - public void onDetachedFromEngine(@NonNull FlutterPluginBinding binding) { - if (flutterCookieManager == null) { - return; - } - - flutterCookieManager.dispose(); - flutterCookieManager = null; - } + public void onDetachedFromEngine(@NonNull FlutterPluginBinding binding) {} @Override public void onAttachedToActivity(@NonNull ActivityPluginBinding activityPluginBinding) {
diff --git a/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/WebViewHostApiImpl.java b/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/WebViewHostApiImpl.java index b557e98..0f31613 100644 --- a/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/WebViewHostApiImpl.java +++ b/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/WebViewHostApiImpl.java
@@ -383,6 +383,12 @@ } @Override + public void postUrl(Long instanceId, String url, byte[] data) { + final WebView webView = (WebView) instanceManager.getInstance(instanceId); + webView.postUrl(url, data); + } + + @Override public String getUrl(Long instanceId) { final WebView webView = (WebView) instanceManager.getInstance(instanceId); final String result = webView.getUrl(); @@ -502,6 +508,12 @@ webView.setWebChromeClient((WebChromeClient) instanceManager.getInstance(clientInstanceId)); } + @Override + public void setBackgroundColor(Long instanceId, Long color) { + final WebView webView = (WebView) instanceManager.getInstance(instanceId); + webView.setBackgroundColor(color.intValue()); + } + @Nullable private static String parseNullStringIdentifier(String value) { if (value.equals(nullStringIdentifier)) {
diff --git a/webview_flutter_android/android/src/test/java/io/flutter/plugins/webviewflutter/CookieManagerHostApiImplTest.java b/webview_flutter_android/android/src/test/java/io/flutter/plugins/webviewflutter/CookieManagerHostApiImplTest.java new file mode 100644 index 0000000..6daeb1b --- /dev/null +++ b/webview_flutter_android/android/src/test/java/io/flutter/plugins/webviewflutter/CookieManagerHostApiImplTest.java
@@ -0,0 +1,83 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package io.flutter.plugins.webviewflutter; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.doAnswer; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.mockStatic; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import android.os.Build; +import android.webkit.CookieManager; +import android.webkit.ValueCallback; +import io.flutter.plugins.webviewflutter.utils.TestUtils; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.mockito.MockedStatic; + +public class CookieManagerHostApiImplTest { + + private CookieManager cookieManager; + private MockedStatic<CookieManager> staticMockCookieManager; + + @Before + public void setup() { + staticMockCookieManager = mockStatic(CookieManager.class); + cookieManager = mock(CookieManager.class); + when(CookieManager.getInstance()).thenReturn(cookieManager); + when(cookieManager.hasCookies()).thenReturn(true); + doAnswer( + answer -> { + ((ValueCallback<Boolean>) answer.getArgument(0)).onReceiveValue(true); + return null; + }) + .when(cookieManager) + .removeAllCookies(any()); + } + + @After + public void tearDown() { + staticMockCookieManager.close(); + } + + @Test + public void setCookieShouldCallSetCookie() { + // Setup + CookieManagerHostApiImpl impl = new CookieManagerHostApiImpl(); + // Run + impl.setCookie("flutter.dev", "foo=bar; path=/"); + // Verify + verify(cookieManager).setCookie("flutter.dev", "foo=bar; path=/"); + } + + @Test + public void clearCookiesShouldCallRemoveAllCookiesOnAndroidLAbove() { + // Setup + TestUtils.setFinalStatic(Build.VERSION.class, "SDK_INT", Build.VERSION_CODES.LOLLIPOP); + GeneratedAndroidWebView.Result<Boolean> result = mock(GeneratedAndroidWebView.Result.class); + CookieManagerHostApiImpl impl = new CookieManagerHostApiImpl(); + // Run + impl.clearCookies(result); + // Verify + verify(cookieManager).removeAllCookies(any()); + verify(result).success(true); + } + + @Test + public void clearCookiesShouldCallRemoveAllCookieBelowAndroidL() { + // Setup + TestUtils.setFinalStatic(Build.VERSION.class, "SDK_INT", Build.VERSION_CODES.KITKAT_WATCH); + GeneratedAndroidWebView.Result<Boolean> result = mock(GeneratedAndroidWebView.Result.class); + CookieManagerHostApiImpl impl = new CookieManagerHostApiImpl(); + // Run + impl.clearCookies(result); + // Verify + verify(cookieManager).removeAllCookie(); + verify(result).success(true); + } +}
diff --git a/webview_flutter_android/android/src/test/java/io/flutter/plugins/webviewflutter/FlutterAssetManagerHostApiImplTest.java b/webview_flutter_android/android/src/test/java/io/flutter/plugins/webviewflutter/FlutterAssetManagerHostApiImplTest.java new file mode 100644 index 0000000..f530365 --- /dev/null +++ b/webview_flutter_android/android/src/test/java/io/flutter/plugins/webviewflutter/FlutterAssetManagerHostApiImplTest.java
@@ -0,0 +1,76 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package io.flutter.plugins.webviewflutter; + +import static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.fail; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import java.io.IOException; +import java.util.List; +import org.junit.Before; +import org.junit.Test; +import org.mockito.Mock; + +public class FlutterAssetManagerHostApiImplTest { + @Mock FlutterAssetManager mockFlutterAssetManager; + + FlutterAssetManagerHostApiImpl testFlutterAssetManagerHostApiImpl; + + @Before + public void setUp() { + mockFlutterAssetManager = mock(FlutterAssetManager.class); + + testFlutterAssetManagerHostApiImpl = + new FlutterAssetManagerHostApiImpl(mockFlutterAssetManager); + } + + @Test + public void list() { + try { + when(mockFlutterAssetManager.list("test/path")) + .thenReturn(new String[] {"index.html", "styles.css"}); + List<String> actualFilePaths = testFlutterAssetManagerHostApiImpl.list("test/path"); + verify(mockFlutterAssetManager).list("test/path"); + assertArrayEquals(new String[] {"index.html", "styles.css"}, actualFilePaths.toArray()); + } catch (IOException ex) { + fail(); + } + } + + @Test + public void list_returns_empty_list_when_no_results() { + try { + when(mockFlutterAssetManager.list("test/path")).thenReturn(null); + List<String> actualFilePaths = testFlutterAssetManagerHostApiImpl.list("test/path"); + verify(mockFlutterAssetManager).list("test/path"); + assertArrayEquals(new String[] {}, actualFilePaths.toArray()); + } catch (IOException ex) { + fail(); + } + } + + @Test(expected = RuntimeException.class) + public void list_should_convert_io_exception_to_runtime_exception() { + try { + when(mockFlutterAssetManager.list("test/path")).thenThrow(new IOException()); + testFlutterAssetManagerHostApiImpl.list("test/path"); + } catch (IOException ex) { + fail(); + } + } + + @Test + public void getAssetFilePathByName() { + when(mockFlutterAssetManager.getAssetFilePathByName("index.html")) + .thenReturn("flutter_assets/index.html"); + String filePath = testFlutterAssetManagerHostApiImpl.getAssetFilePathByName("index.html"); + verify(mockFlutterAssetManager).getAssetFilePathByName("index.html"); + assertEquals("flutter_assets/index.html", filePath); + } +}
diff --git a/webview_flutter_android/android/src/test/java/io/flutter/plugins/webviewflutter/PluginBindingFlutterAssetManagerTest.java b/webview_flutter_android/android/src/test/java/io/flutter/plugins/webviewflutter/PluginBindingFlutterAssetManagerTest.java new file mode 100644 index 0000000..1f556b7 --- /dev/null +++ b/webview_flutter_android/android/src/test/java/io/flutter/plugins/webviewflutter/PluginBindingFlutterAssetManagerTest.java
@@ -0,0 +1,54 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package io.flutter.plugins.webviewflutter; + +import static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.fail; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import android.content.res.AssetManager; +import io.flutter.embedding.engine.plugins.FlutterPlugin.FlutterAssets; +import io.flutter.plugins.webviewflutter.FlutterAssetManager.PluginBindingFlutterAssetManager; +import java.io.IOException; +import org.junit.Before; +import org.junit.Test; +import org.mockito.Mock; + +public class PluginBindingFlutterAssetManagerTest { + @Mock AssetManager mockAssetManager; + @Mock FlutterAssets mockFlutterAssets; + + PluginBindingFlutterAssetManager tesPluginBindingFlutterAssetManager; + + @Before + public void setUp() { + mockAssetManager = mock(AssetManager.class); + mockFlutterAssets = mock(FlutterAssets.class); + + tesPluginBindingFlutterAssetManager = + new PluginBindingFlutterAssetManager(mockAssetManager, mockFlutterAssets); + } + + @Test + public void list() { + try { + when(mockAssetManager.list("test/path")) + .thenReturn(new String[] {"index.html", "styles.css"}); + String[] actualFilePaths = tesPluginBindingFlutterAssetManager.list("test/path"); + verify(mockAssetManager).list("test/path"); + assertArrayEquals(new String[] {"index.html", "styles.css"}, actualFilePaths); + } catch (IOException ex) { + fail(); + } + } + + @Test + public void registrar_getAssetFilePathByName() { + tesPluginBindingFlutterAssetManager.getAssetFilePathByName("sample_movie.mp4"); + verify(mockFlutterAssets).getAssetFilePathByName("sample_movie.mp4"); + } +}
diff --git a/webview_flutter_android/android/src/test/java/io/flutter/plugins/webviewflutter/RegistrarFlutterAssetManagerTest.java b/webview_flutter_android/android/src/test/java/io/flutter/plugins/webviewflutter/RegistrarFlutterAssetManagerTest.java new file mode 100644 index 0000000..86b0fb5 --- /dev/null +++ b/webview_flutter_android/android/src/test/java/io/flutter/plugins/webviewflutter/RegistrarFlutterAssetManagerTest.java
@@ -0,0 +1,55 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package io.flutter.plugins.webviewflutter; + +import static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.fail; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import android.content.res.AssetManager; +import io.flutter.plugin.common.PluginRegistry.Registrar; +import io.flutter.plugins.webviewflutter.FlutterAssetManager.RegistrarFlutterAssetManager; +import java.io.IOException; +import org.junit.Before; +import org.junit.Test; +import org.mockito.Mock; + +@SuppressWarnings("deprecation") +public class RegistrarFlutterAssetManagerTest { + @Mock AssetManager mockAssetManager; + @Mock Registrar mockRegistrar; + + RegistrarFlutterAssetManager testRegistrarFlutterAssetManager; + + @Before + public void setUp() { + mockAssetManager = mock(AssetManager.class); + mockRegistrar = mock(Registrar.class); + + testRegistrarFlutterAssetManager = + new RegistrarFlutterAssetManager(mockAssetManager, mockRegistrar); + } + + @Test + public void list() { + try { + when(mockAssetManager.list("test/path")) + .thenReturn(new String[] {"index.html", "styles.css"}); + String[] actualFilePaths = testRegistrarFlutterAssetManager.list("test/path"); + verify(mockAssetManager).list("test/path"); + assertArrayEquals(new String[] {"index.html", "styles.css"}, actualFilePaths); + } catch (IOException ex) { + fail(); + } + } + + @Test + public void registrar_getAssetFilePathByName() { + testRegistrarFlutterAssetManager.getAssetFilePathByName("sample_movie.mp4"); + verify(mockRegistrar).lookupKeyForAsset("sample_movie.mp4"); + } +}
diff --git a/webview_flutter_android/android/src/test/java/io/flutter/plugins/webviewflutter/WebViewTest.java b/webview_flutter_android/android/src/test/java/io/flutter/plugins/webviewflutter/WebViewTest.java index dc58b9b..2312b76 100644 --- a/webview_flutter_android/android/src/test/java/io/flutter/plugins/webviewflutter/WebViewTest.java +++ b/webview_flutter_android/android/src/test/java/io/flutter/plugins/webviewflutter/WebViewTest.java
@@ -209,6 +209,12 @@ } @Test + public void postUrl() { + testHostApiImpl.postUrl(0L, "https://www.google.com", new byte[] {0x01, 0x02}); + verify(mockWebView).postUrl("https://www.google.com", new byte[] {0x01, 0x02}); + } + + @Test public void getUrl() { when(mockWebView.getUrl()).thenReturn("https://www.google.com"); assertEquals(testHostApiImpl.getUrl(0L), "https://www.google.com");
diff --git a/webview_flutter_android/android/src/test/java/io/flutter/plugins/webviewflutter/utils/TestUtils.java b/webview_flutter_android/android/src/test/java/io/flutter/plugins/webviewflutter/utils/TestUtils.java new file mode 100644 index 0000000..31e7d58 --- /dev/null +++ b/webview_flutter_android/android/src/test/java/io/flutter/plugins/webviewflutter/utils/TestUtils.java
@@ -0,0 +1,47 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package io.flutter.plugins.webviewflutter.utils; + +import java.lang.reflect.Field; +import java.lang.reflect.Modifier; +import org.junit.Assert; + +public class TestUtils { + public static <T> void setFinalStatic(Class<T> classToModify, String fieldName, Object newValue) { + try { + Field field = classToModify.getField(fieldName); + field.setAccessible(true); + + Field modifiersField = Field.class.getDeclaredField("modifiers"); + modifiersField.setAccessible(true); + modifiersField.setInt(field, field.getModifiers() & ~Modifier.FINAL); + + field.set(null, newValue); + } catch (Exception e) { + Assert.fail("Unable to mock static field: " + fieldName); + } + } + + public static <T> void setPrivateField(T instance, String fieldName, Object newValue) { + try { + Field field = instance.getClass().getDeclaredField(fieldName); + field.setAccessible(true); + field.set(instance, newValue); + } catch (Exception e) { + Assert.fail("Unable to mock private field: " + fieldName); + } + } + + public static <T> Object getPrivateField(T instance, String fieldName) { + try { + Field field = instance.getClass().getDeclaredField(fieldName); + field.setAccessible(true); + return field.get(instance); + } catch (Exception e) { + Assert.fail("Unable to mock private field: " + fieldName); + return null; + } + } +}
diff --git a/webview_flutter_android/example/android/app/src/androidTest/java/io/flutter/plugins/webviewflutterexample/BackgroundColorTest.java b/webview_flutter_android/example/android/app/src/androidTest/java/io/flutter/plugins/webviewflutterexample/BackgroundColorTest.java new file mode 100644 index 0000000..a63629e --- /dev/null +++ b/webview_flutter_android/example/android/app/src/androidTest/java/io/flutter/plugins/webviewflutterexample/BackgroundColorTest.java
@@ -0,0 +1,61 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package io.flutter.plugins.webviewflutterexample; + +import static androidx.test.espresso.flutter.EspressoFlutter.onFlutterWidget; +import static androidx.test.espresso.flutter.action.FlutterActions.click; +import static androidx.test.espresso.flutter.matcher.FlutterMatchers.withText; +import static androidx.test.espresso.flutter.matcher.FlutterMatchers.withValueKey; +import static org.junit.Assert.assertEquals; + +import android.graphics.Bitmap; +import android.graphics.Color; +import androidx.test.core.app.ActivityScenario; +import androidx.test.ext.junit.runners.AndroidJUnit4; +import androidx.test.rule.ActivityTestRule; +import androidx.test.runner.screenshot.ScreenCapture; +import androidx.test.runner.screenshot.Screenshot; +import org.junit.Before; +import org.junit.Ignore; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; + +@RunWith(AndroidJUnit4.class) +public class BackgroundColorTest { + @Rule + public ActivityTestRule<DriverExtensionActivity> myActivityTestRule = + new ActivityTestRule<>(DriverExtensionActivity.class, true, false); + + @Before + public void setUp() { + ActivityScenario.launch(DriverExtensionActivity.class); + } + + @Ignore("Doesn't run in Firebase Test Lab: https://github.com/flutter/flutter/issues/94748") + @Test + public void backgroundColor() { + onFlutterWidget(withValueKey("ShowPopupMenu")).perform(click()); + onFlutterWidget(withValueKey("ShowTransparentBackgroundExample")).perform(click()); + onFlutterWidget(withText("Transparent background test")); + + final ScreenCapture screenCapture = Screenshot.capture(); + final Bitmap screenBitmap = screenCapture.getBitmap(); + + final int centerLeftColor = + screenBitmap.getPixel(10, (int) Math.floor(screenBitmap.getHeight() / 2.0)); + final int centerColor = + screenBitmap.getPixel( + (int) Math.floor(screenBitmap.getWidth() / 2.0), + (int) Math.floor(screenBitmap.getHeight() / 2.0)); + + // Flutter Colors.green color : 0xFF4CAF50 + // https://github.com/flutter/flutter/blob/f4abaa0735eba4dfd8f33f73363911d63931fe03/packages/flutter/lib/src/material/colors.dart#L1208 + // The background color of the webview is : rgba(0, 0, 0, 0.5) + // The expected color is : rgba(38, 87, 40, 1) -> 0xFF265728 + assertEquals(0xFF265728, centerLeftColor); + assertEquals(Color.RED, centerColor); + } +}
diff --git a/webview_flutter_android/example/android/app/src/debug/AndroidManifest.xml b/webview_flutter_android/example/android/app/src/debug/AndroidManifest.xml index 2879220..110b9ab 100644 --- a/webview_flutter_android/example/android/app/src/debug/AndroidManifest.xml +++ b/webview_flutter_android/example/android/app/src/debug/AndroidManifest.xml
@@ -13,5 +13,13 @@ android:hardwareAccelerated="true" android:windowSoftInputMode="adjustResize"> </activity> + <activity + android:name=".DriverExtensionActivity" + android:launchMode="singleTop" + android:theme="@style/LaunchTheme" + android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode" + android:hardwareAccelerated="true" + android:windowSoftInputMode="adjustResize"> + </activity> </application> </manifest>
diff --git a/webview_flutter_android/example/android/app/src/main/java/io/flutter/plugins/webviewflutterexample/DriverExtensionActivity.java b/webview_flutter_android/example/android/app/src/main/java/io/flutter/plugins/webviewflutterexample/DriverExtensionActivity.java new file mode 100644 index 0000000..59e1b04 --- /dev/null +++ b/webview_flutter_android/example/android/app/src/main/java/io/flutter/plugins/webviewflutterexample/DriverExtensionActivity.java
@@ -0,0 +1,16 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package io.flutter.plugins.webviewflutterexample; + +import androidx.annotation.NonNull; +import io.flutter.embedding.android.FlutterActivity; + +public class DriverExtensionActivity extends FlutterActivity { + @Override + @NonNull + public String getDartEntrypointFunctionName() { + return "appMain"; + } +}
diff --git a/webview_flutter_android/example/lib/main.dart b/webview_flutter_android/example/lib/main.dart index bc43c16..0d0cd59 100644 --- a/webview_flutter_android/example/lib/main.dart +++ b/webview_flutter_android/example/lib/main.dart
@@ -7,8 +7,10 @@ import 'dart:async'; import 'dart:convert'; import 'dart:io'; +import 'dart:typed_data'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; +import 'package:flutter_driver/driver_extension.dart'; import 'package:path_provider/path_provider.dart'; import 'package:webview_flutter_android/webview_surface_android.dart'; import 'package:webview_flutter_platform_interface/webview_flutter_platform_interface.dart'; @@ -17,6 +19,11 @@ import 'navigation_request.dart'; import 'web_view.dart'; +void appMain() { + enableFlutterDriverExtension(); + main(); +} + void main() { // Configure the [WebView] to use the [SurfaceAndroidWebView] // implementation instead of the default [AndroidWebView]. @@ -59,6 +66,27 @@ </html> '''; +const String kTransparentBackgroundPage = ''' +<!DOCTYPE html> +<html> +<head> + <title>Transparent background test</title> +</head> +<style type="text/css"> + body { background: transparent; margin: 0; padding: 0; } + #container { position: relative; margin: 0; padding: 0; width: 100vw; height: 100vh; } + #shape { background: #FF0000; width: 200px; height: 100%; margin: 0; padding: 0; position: absolute; top: 0; bottom: 0; left: calc(50% - 100px); } + p { text-align: center; } +</style> +<body> + <div id="container"> + <p>Transparent background test</p> + <div id="shape"></div> + </div> +</body> +</html> +'''; + class _WebViewExample extends StatefulWidget { const _WebViewExample({Key? key}) : super(key: key); @@ -73,6 +101,7 @@ @override Widget build(BuildContext context) { return Scaffold( + backgroundColor: const Color(0xFF4CAF50), appBar: AppBar( title: const Text('Flutter WebView example'), // This drop down menu demonstrates that Flutter widgets can be shown over the web view. @@ -109,6 +138,7 @@ javascriptChannels: _createJavascriptChannels(context), javascriptMode: JavascriptMode.unrestricted, userAgent: 'Custom_User_Agent', + backgroundColor: const Color(0x80000000), ); }), floatingActionButton: favoriteButton(), @@ -156,8 +186,12 @@ listCache, clearCache, navigationDelegate, + loadFlutterAsset, loadLocalFile, loadHtmlString, + transparentBackground, + doPostRequest, + setCookie, } class _SampleMenu extends StatelessWidget { @@ -172,6 +206,7 @@ builder: (BuildContext context, AsyncSnapshot<WebViewController> controller) { return PopupMenuButton<_MenuOptions>( + key: const ValueKey<String>('ShowPopupMenu'), onSelected: (_MenuOptions value) { switch (value) { case _MenuOptions.showUserAgent: @@ -195,12 +230,24 @@ case _MenuOptions.navigationDelegate: _onNavigationDelegateExample(controller.data!, context); break; + case _MenuOptions.loadFlutterAsset: + _onLoadFlutterAssetExample(controller.data!, context); + break; case _MenuOptions.loadLocalFile: _onLoadLocalFileExample(controller.data!, context); break; case _MenuOptions.loadHtmlString: _onLoadHtmlStringExample(controller.data!, context); break; + case _MenuOptions.transparentBackground: + _onTransparentBackground(controller.data!, context); + break; + case _MenuOptions.doPostRequest: + _onDoPostRequest(controller.data!, context); + break; + case _MenuOptions.setCookie: + _onSetCookie(controller.data!, context); + break; } }, itemBuilder: (BuildContext context) => <PopupMenuItem<_MenuOptions>>[ @@ -234,6 +281,10 @@ child: Text('Navigation Delegate example'), ), const PopupMenuItem<_MenuOptions>( + value: _MenuOptions.loadFlutterAsset, + child: Text('Load Flutter Asset'), + ), + const PopupMenuItem<_MenuOptions>( value: _MenuOptions.loadHtmlString, child: Text('Load HTML string'), ), @@ -241,6 +292,19 @@ value: _MenuOptions.loadLocalFile, child: Text('Load local file'), ), + const PopupMenuItem<_MenuOptions>( + key: ValueKey<String>('ShowTransparentBackgroundExample'), + value: _MenuOptions.transparentBackground, + child: Text('Transparent background example'), + ), + const PopupMenuItem<_MenuOptions>( + value: _MenuOptions.doPostRequest, + child: Text('Post Request'), + ), + const PopupMenuItem<_MenuOptions>( + value: _MenuOptions.setCookie, + child: Text('Set Cookie'), + ), ], ); }, @@ -300,7 +364,7 @@ Future<void> _onClearCookies( WebViewController controller, BuildContext context) async { - final bool hadCookies = await WebView.platform.clearCookies(); + final bool hadCookies = await WebViewCookieManager.instance.clearCookies(); String message = 'There were cookies. Now, they are gone!'; if (!hadCookies) { message = 'There are no cookies.'; @@ -311,6 +375,15 @@ )); } + Future<void> _onSetCookie( + WebViewController controller, BuildContext context) async { + await WebViewCookieManager.instance.setCookie( + const WebViewCookie( + name: 'foo', value: 'bar', domain: 'httpbin.org', path: '/anything'), + ); + await controller.loadUrl('https://httpbin.org/anything'); + } + Future<void> _onNavigationDelegateExample( WebViewController controller, BuildContext context) async { final String contentBase64 = @@ -318,6 +391,11 @@ await controller.loadUrl('data:text/html;base64,$contentBase64'); } + Future<void> _onLoadFlutterAssetExample( + WebViewController controller, BuildContext context) async { + await controller.loadFlutterAsset('assets/www/index.html'); + } + Future<void> _onLoadLocalFileExample( WebViewController controller, BuildContext context) async { final String pathToIndex = await _prepareLocalFile(); @@ -330,6 +408,16 @@ await controller.loadHtmlString(kExamplePage); } + Future<void> _onDoPostRequest( + WebViewController controller, BuildContext context) async { + final WebViewRequest request = WebViewRequest( + uri: Uri.parse('https://httpbin.org/post'), + method: WebViewRequestMethod.post, + body: Uint8List.fromList('Test Body'.codeUnits), + ); + await controller.loadRequest(request); + } + Widget _getCookieList(String cookies) { if (cookies == null || cookies == '""') { return Container(); @@ -353,6 +441,11 @@ return indexFile.path; } + + Future<void> _onTransparentBackground( + WebViewController controller, BuildContext context) async { + await controller.loadHtmlString(kTransparentBackgroundPage); + } } class _NavigationControls extends StatelessWidget {
diff --git a/webview_flutter_android/example/lib/web_view.dart b/webview_flutter_android/example/lib/web_view.dart index 654abbd..91ea663 100644 --- a/webview_flutter_android/example/lib/web_view.dart +++ b/webview_flutter_android/example/lib/web_view.dart
@@ -3,11 +3,13 @@ // found in the LICENSE file. import 'dart:async'; +import 'dart:io'; import 'package:flutter/foundation.dart'; import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; import 'package:webview_flutter_android/webview_android.dart'; +import 'package:webview_flutter_android/webview_android_cookie_manager.dart'; import 'package:webview_flutter_platform_interface/webview_flutter_platform_interface.dart'; import 'navigation_decision.dart'; @@ -61,6 +63,7 @@ Key? key, this.onWebViewCreated, this.initialUrl, + this.initialCookies = const <WebViewCookie>[], this.javascriptMode = JavascriptMode.disabled, this.javascriptChannels, this.navigationDelegate, @@ -76,6 +79,7 @@ this.initialMediaPlaybackPolicy = AutoMediaPlaybackPolicy.require_user_action_for_all_media_types, this.allowsInlineMediaPlayback = false, + this.backgroundColor, }) : assert(javascriptMode != null), assert(initialMediaPlaybackPolicy != null), assert(allowsInlineMediaPlayback != null), @@ -103,6 +107,9 @@ /// The initial URL to load. final String? initialUrl; + /// The initial cookies to set. + final List<WebViewCookie> initialCookies; + /// Whether JavaScript execution is enabled. final JavascriptMode javascriptMode; @@ -236,6 +243,12 @@ /// The default policy is [AutoMediaPlaybackPolicy.require_user_action_for_all_media_types]. final AutoMediaPlaybackPolicy initialMediaPlaybackPolicy; + /// The background color of the [WebView]. + /// + /// When `null` the platform's webview default background color is used. By + /// default [backgroundColor] is `null`. + final Color? backgroundColor; + @override _WebViewState createState() => _WebViewState(); } @@ -287,6 +300,8 @@ _javascriptChannelRegistry.channels.keys.toSet(), autoMediaPlaybackPolicy: widget.initialMediaPlaybackPolicy, userAgent: widget.userAgent, + backgroundColor: widget.backgroundColor, + cookies: widget.initialCookies, ), javascriptChannelRegistry: _javascriptChannelRegistry, ); @@ -374,6 +389,14 @@ return _webViewPlatformController.loadFile(absoluteFilePath); } + /// Loads the Flutter asset specified in the pubspec.yaml file. + /// + /// Throws an ArgumentError if [key] is not part of the specified assets + /// in the pubspec.yaml file. + Future<void> loadFlutterAsset(String key) { + return _webViewPlatformController.loadFlutterAsset(key); + } + /// Loads the supplied HTML string. /// /// The [baseUrl] parameter is used when resolving relative URLs within the @@ -402,6 +425,11 @@ return _webViewPlatformController.loadUrl(url, headers); } + /// Loads a page by making the specified request. + Future<void> loadRequest(WebViewRequest request) async { + return _webViewPlatformController.loadRequest(request); + } + /// Accessor to the current URL that the WebView is displaying. /// /// If [WebView.initialUrl] was never specified, returns `null`. @@ -653,3 +681,21 @@ zoomEnabled: widget.zoomEnabled, ); } + +/// App-facing cookie manager that exposes the correct platform implementation. +class WebViewCookieManager extends WebViewCookieManagerPlatform { + WebViewCookieManager._(); + + /// Returns an instance of the cookie manager for the current platform. + static WebViewCookieManagerPlatform get instance { + if (WebViewCookieManagerPlatform.instance == null) { + if (Platform.isAndroid) { + WebViewCookieManagerPlatform.instance = WebViewAndroidCookieManager(); + } else { + throw AssertionError( + 'This platform is currently unsupported for webview_flutter_android.'); + } + } + return WebViewCookieManagerPlatform.instance!; + } +}
diff --git a/webview_flutter_android/example/pubspec.yaml b/webview_flutter_android/example/pubspec.yaml index 59579df..85990bd 100644 --- a/webview_flutter_android/example/pubspec.yaml +++ b/webview_flutter_android/example/pubspec.yaml
@@ -34,3 +34,5 @@ assets: - assets/sample_audio.ogg - assets/sample_video.mp4 + - assets/www/index.html + - assets/www/styles/style.css
diff --git a/webview_flutter_android/lib/src/android_webview.dart b/webview_flutter_android/lib/src/android_webview.dart index 928cdb3..dfa05cd 100644 --- a/webview_flutter_android/lib/src/android_webview.dart +++ b/webview_flutter_android/lib/src/android_webview.dart
@@ -2,9 +2,13 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. +import 'dart:typed_data'; +import 'dart:ui'; + import 'package:flutter/foundation.dart'; import 'package:flutter/widgets.dart' show AndroidViewSurface; +import 'android_webview.pigeon.dart'; import 'android_webview_api_impls.dart'; // TODO(bparrishMines): This can be removed once pigeon supports null values: https://github.com/flutter/flutter/issues/59118 @@ -166,6 +170,13 @@ return api.loadUrlFromInstance(this, url, headers); } + /// Loads the URL with postData using "POST" method into this WebView. + /// + /// If url is not a network URL, it will be loaded with [loadUrl] instead, ignoring the postData param. + Future<void> postUrl(String url, Uint8List data) { + return api.postUrlFromInstance(this, url, data); + } + /// Gets the URL for the current page. /// /// This is not always the same as the URL passed to @@ -349,6 +360,11 @@ return api.setWebChromeClientFromInstance(this, client); } + /// Sets the background color of this WebView. + Future<void> setBackgroundColor(Color color) { + return api.setBackgroundColorFromInstance(this, color.value); + } + /// Releases all resources used by the [WebView]. /// /// Any methods called after [release] will throw an exception. @@ -359,6 +375,49 @@ } } +/// Manages cookies globally for all webviews. +class CookieManager { + CookieManager._(); + + static CookieManager? _instance; + + /// Gets the globally set CookieManager instance. + static CookieManager get instance => _instance ??= CookieManager._(); + + /// Setter for the singleton value, for testing purposes only. + @visibleForTesting + static set instance(CookieManager value) => _instance = value; + + /// Pigeon Host Api implementation for [CookieManager]. + @visibleForTesting + static CookieManagerHostApi api = CookieManagerHostApi(); + + /// Sets a single cookie (key-value pair) for the given URL. Any existing + /// cookie with the same host, path and name will be replaced with the new + /// cookie. The cookie being set will be ignored if it is expired. To set + /// multiple cookies, your application should invoke this method multiple + /// times. + /// + /// The value parameter must follow the format of the Set-Cookie HTTP + /// response header defined by RFC6265bis. This is a key-value pair of the + /// form "key=value", optionally followed by a list of cookie attributes + /// delimited with semicolons (ex. "key=value; Max-Age=123"). Please consult + /// the RFC specification for a list of valid attributes. + /// + /// Note: if specifying a value containing the "Secure" attribute, url must + /// use the "https://" scheme. + /// + /// Params: + /// url – the URL for which the cookie is to be set + /// value – the cookie as a string, using the format of the 'Set-Cookie' HTTP response header + Future<void> setCookie(String url, String value) => api.setCookie(url, value); + + /// Removes all cookies. + /// + /// The returned future resolves to true if any cookies were removed. + Future<bool> clearCookies() => api.clearCookies(); +} + /// Manages settings state for a [WebView]. /// /// When a WebView is first created, it obtains a set of default settings. These @@ -763,3 +822,23 @@ /// Describes the error. final String description; } + +/// Manages Flutter assets that are part of Android's app bundle. +class FlutterAssetManager { + /// Constructs the [FlutterAssetManager]. + const FlutterAssetManager(); + + /// Pigeon Host Api implementation for [FlutterAssetManager]. + @visibleForTesting + static FlutterAssetManagerHostApi api = FlutterAssetManagerHostApi(); + + /// Lists all assets at the given path. + /// + /// The assets are returned as a `List<String>`. The `List<String>` only + /// contains files which are direct childs + Future<List<String?>> list(String path) => api.list(path); + + /// Gets the relative file path to the Flutter asset with the given name. + Future<String> getAssetFilePathByName(String name) => + api.getAssetFilePathByName(name); +}
diff --git a/webview_flutter_android/lib/src/android_webview.pigeon.dart b/webview_flutter_android/lib/src/android_webview.pigeon.dart index 9bf2de6..810a717 100644 --- a/webview_flutter_android/lib/src/android_webview.pigeon.dart +++ b/webview_flutter_android/lib/src/android_webview.pigeon.dart
@@ -63,6 +63,72 @@ } } +class _CookieManagerHostApiCodec extends StandardMessageCodec { + const _CookieManagerHostApiCodec(); +} + +class CookieManagerHostApi { + /// Constructor for [CookieManagerHostApi]. The [binaryMessenger] named argument is + /// available for dependency injection. If it is left null, the default + /// BinaryMessenger will be used which routes to the host platform. + CookieManagerHostApi({BinaryMessenger? binaryMessenger}) + : _binaryMessenger = binaryMessenger; + + final BinaryMessenger? _binaryMessenger; + + static const MessageCodec<Object?> codec = _CookieManagerHostApiCodec(); + + Future<bool> clearCookies() async { + final BasicMessageChannel<Object?> channel = BasicMessageChannel<Object?>( + 'dev.flutter.pigeon.CookieManagerHostApi.clearCookies', codec, + binaryMessenger: _binaryMessenger); + final Map<Object?, Object?>? replyMap = + await channel.send(null) as Map<Object?, Object?>?; + if (replyMap == null) { + throw PlatformException( + code: 'channel-error', + message: 'Unable to establish connection on channel.', + details: null, + ); + } else if (replyMap['error'] != null) { + final Map<Object?, Object?> error = + (replyMap['error'] as Map<Object?, Object?>?)!; + throw PlatformException( + code: (error['code'] as String?)!, + message: error['message'] as String?, + details: error['details'], + ); + } else { + return (replyMap['result'] as bool?)!; + } + } + + Future<void> setCookie(String arg_url, String arg_value) async { + final BasicMessageChannel<Object?> channel = BasicMessageChannel<Object?>( + 'dev.flutter.pigeon.CookieManagerHostApi.setCookie', codec, + binaryMessenger: _binaryMessenger); + final Map<Object?, Object?>? replyMap = await channel + .send(<Object>[arg_url, arg_value]) as Map<Object?, Object?>?; + if (replyMap == null) { + throw PlatformException( + code: 'channel-error', + message: 'Unable to establish connection on channel.', + details: null, + ); + } else if (replyMap['error'] != null) { + final Map<Object?, Object?> error = + (replyMap['error'] as Map<Object?, Object?>?)!; + throw PlatformException( + code: (error['code'] as String?)!, + message: error['message'] as String?, + details: error['details'], + ); + } else { + return; + } + } +} + class _WebViewHostApiCodec extends StandardMessageCodec { const _WebViewHostApiCodec(); } @@ -220,6 +286,33 @@ } } + Future<void> postUrl( + int arg_instanceId, String arg_url, Uint8List arg_data) async { + final BasicMessageChannel<Object?> channel = BasicMessageChannel<Object?>( + 'dev.flutter.pigeon.WebViewHostApi.postUrl', codec, + binaryMessenger: _binaryMessenger); + final Map<Object?, Object?>? replyMap = + await channel.send(<Object>[arg_instanceId, arg_url, arg_data]) + as Map<Object?, Object?>?; + if (replyMap == null) { + throw PlatformException( + code: 'channel-error', + message: 'Unable to establish connection on channel.', + details: null, + ); + } else if (replyMap['error'] != null) { + final Map<Object?, Object?> error = + (replyMap['error'] as Map<Object?, Object?>?)!; + throw PlatformException( + code: (error['code'] as String?)!, + message: error['message'] as String?, + details: error['details'], + ); + } else { + return; + } + } + Future<String> getUrl(int arg_instanceId) async { final BasicMessageChannel<Object?> channel = BasicMessageChannel<Object?>( 'dev.flutter.pigeon.WebViewHostApi.getUrl', codec, @@ -708,6 +801,31 @@ return; } } + + Future<void> setBackgroundColor(int arg_instanceId, int arg_color) async { + final BasicMessageChannel<Object?> channel = BasicMessageChannel<Object?>( + 'dev.flutter.pigeon.WebViewHostApi.setBackgroundColor', codec, + binaryMessenger: _binaryMessenger); + final Map<Object?, Object?>? replyMap = await channel + .send(<Object>[arg_instanceId, arg_color]) as Map<Object?, Object?>?; + if (replyMap == null) { + throw PlatformException( + code: 'channel-error', + message: 'Unable to establish connection on channel.', + details: null, + ); + } else if (replyMap['error'] != null) { + final Map<Object?, Object?> error = + (replyMap['error'] as Map<Object?, Object?>?)!; + throw PlatformException( + code: (error['code'] as String?)!, + message: error['message'] as String?, + details: error['details'], + ); + } else { + return; + } + } } class _WebSettingsHostApiCodec extends StandardMessageCodec { @@ -1591,6 +1709,73 @@ } } +class _FlutterAssetManagerHostApiCodec extends StandardMessageCodec { + const _FlutterAssetManagerHostApiCodec(); +} + +class FlutterAssetManagerHostApi { + /// Constructor for [FlutterAssetManagerHostApi]. The [binaryMessenger] named argument is + /// available for dependency injection. If it is left null, the default + /// BinaryMessenger will be used which routes to the host platform. + FlutterAssetManagerHostApi({BinaryMessenger? binaryMessenger}) + : _binaryMessenger = binaryMessenger; + + final BinaryMessenger? _binaryMessenger; + + static const MessageCodec<Object?> codec = _FlutterAssetManagerHostApiCodec(); + + Future<List<String?>> list(String arg_path) async { + final BasicMessageChannel<Object?> channel = BasicMessageChannel<Object?>( + 'dev.flutter.pigeon.FlutterAssetManagerHostApi.list', codec, + binaryMessenger: _binaryMessenger); + final Map<Object?, Object?>? replyMap = + await channel.send(<Object>[arg_path]) as Map<Object?, Object?>?; + if (replyMap == null) { + throw PlatformException( + code: 'channel-error', + message: 'Unable to establish connection on channel.', + details: null, + ); + } else if (replyMap['error'] != null) { + final Map<Object?, Object?> error = + (replyMap['error'] as Map<Object?, Object?>?)!; + throw PlatformException( + code: (error['code'] as String?)!, + message: error['message'] as String?, + details: error['details'], + ); + } else { + return (replyMap['result'] as List<Object?>?)!.cast<String?>(); + } + } + + Future<String> getAssetFilePathByName(String arg_name) async { + final BasicMessageChannel<Object?> channel = BasicMessageChannel<Object?>( + 'dev.flutter.pigeon.FlutterAssetManagerHostApi.getAssetFilePathByName', + codec, + binaryMessenger: _binaryMessenger); + final Map<Object?, Object?>? replyMap = + await channel.send(<Object>[arg_name]) as Map<Object?, Object?>?; + if (replyMap == null) { + throw PlatformException( + code: 'channel-error', + message: 'Unable to establish connection on channel.', + details: null, + ); + } else if (replyMap['error'] != null) { + final Map<Object?, Object?> error = + (replyMap['error'] as Map<Object?, Object?>?)!; + throw PlatformException( + code: (error['code'] as String?)!, + message: error['message'] as String?, + details: error['details'], + ); + } else { + return (replyMap['result'] as String?)!; + } + } +} + class _WebChromeClientFlutterApiCodec extends StandardMessageCodec { const _WebChromeClientFlutterApiCodec(); }
diff --git a/webview_flutter_android/lib/src/android_webview_api_impls.dart b/webview_flutter_android/lib/src/android_webview_api_impls.dart index ece05b5..1db5ed4 100644 --- a/webview_flutter_android/lib/src/android_webview_api_impls.dart +++ b/webview_flutter_android/lib/src/android_webview_api_impls.dart
@@ -2,6 +2,8 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. +import 'dart:typed_data'; + import 'package:flutter/services.dart'; import 'android_webview.dart'; @@ -153,6 +155,15 @@ } /// Helper method to convert instances ids to objects. + Future<void> postUrlFromInstance( + WebView instance, + String url, + Uint8List data, + ) { + return postUrl(instanceManager.getInstanceId(instance)!, url, data); + } + + /// Helper method to convert instances ids to objects. Future<String> getUrlFromInstance(WebView instance) { return getUrl(instanceManager.getInstanceId(instance)!); } @@ -278,6 +289,11 @@ instanceManager.getInstanceId(client)!, ); } + + /// Helper method to convert instances ids to objects. + Future<void> setBackgroundColorFromInstance(WebView instance, int color) { + return setBackgroundColor(instanceManager.getInstanceId(instance)!, color); + } } /// Host api implementation for [WebSettings].
diff --git a/webview_flutter_android/lib/webview_android.dart b/webview_flutter_android/lib/webview_android.dart index ee474b9..1f0eb7b 100644 --- a/webview_flutter_android/lib/webview_android.dart +++ b/webview_flutter_android/lib/webview_android.dart
@@ -65,5 +65,11 @@ } @override - Future<bool> clearCookies() => MethodChannelWebViewPlatform.clearCookies(); + Future<bool> clearCookies() { + if (WebViewCookieManagerPlatform.instance == null) { + throw Exception( + 'Could not clear cookies as no implementation for WebViewCookieManagerPlatform has been registered.'); + } + return WebViewCookieManagerPlatform.instance!.clearCookies(); + } }
diff --git a/webview_flutter_android/lib/webview_android_cookie_manager.dart b/webview_flutter_android/lib/webview_android_cookie_manager.dart new file mode 100644 index 0000000..bba75ff --- /dev/null +++ b/webview_flutter_android/lib/webview_android_cookie_manager.dart
@@ -0,0 +1,36 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:webview_flutter_android/src/android_webview.dart' + as android_webview; +import 'package:webview_flutter_platform_interface/webview_flutter_platform_interface.dart'; + +/// Handles all cookie operations for the current platform. +class WebViewAndroidCookieManager extends WebViewCookieManagerPlatform { + @override + Future<bool> clearCookies() => + android_webview.CookieManager.instance.clearCookies(); + + @override + Future<void> setCookie(WebViewCookie cookie) { + if (!_isValidPath(cookie.path)) { + throw ArgumentError( + 'The path property for the provided cookie was not given a legal value.'); + } + return android_webview.CookieManager.instance.setCookie( + cookie.domain, + '${Uri.encodeComponent(cookie.name)}=${Uri.encodeComponent(cookie.value)}; path=${cookie.path}', + ); + } + + bool _isValidPath(String path) { + // Permitted ranges based on RFC6265bis: https://datatracker.ietf.org/doc/html/draft-ietf-httpbis-rfc6265bis-02#section-4.1.1 + for (final int char in path.codeUnits) { + if ((char < 0x20 || char > 0x3A) && (char < 0x3C || char > 0x7E)) { + return false; + } + } + return true; + } +}
diff --git a/webview_flutter_android/lib/webview_android_widget.dart b/webview_flutter_android/lib/webview_android_widget.dart index c264bc1..1dec9c1 100644 --- a/webview_flutter_android/lib/webview_android_widget.dart +++ b/webview_flutter_android/lib/webview_android_widget.dart
@@ -3,9 +3,10 @@ // found in the LICENSE file. import 'dart:async'; +import 'dart:typed_data'; import 'package:flutter/widgets.dart'; - +import 'package:webview_flutter_android/webview_android_cookie_manager.dart'; import 'package:webview_flutter_platform_interface/webview_flutter_platform_interface.dart'; import 'src/android_webview.dart' as android_webview; @@ -20,6 +21,8 @@ required this.javascriptChannelRegistry, required this.onBuildWidget, @visibleForTesting this.webViewProxy = const WebViewProxy(), + @visibleForTesting + this.flutterAssetManager = const android_webview.FlutterAssetManager(), }); /// Initial parameters used to setup the WebView. @@ -47,6 +50,11 @@ /// This should only be changed for testing purposes. final WebViewProxy webViewProxy; + /// Manages access to Flutter assets that are part of the Android App bundle. + /// + /// This should only be changed for testing purposes. + final android_webview.FlutterAssetManager flutterAssetManager; + /// Callback to build a widget once [android_webview.WebView] has been initialized. final Widget Function(WebViewAndroidPlatformController controller) onBuildWidget; @@ -67,6 +75,7 @@ callbacksHandler: widget.callbacksHandler, javascriptChannelRegistry: widget.javascriptChannelRegistry, webViewProxy: widget.webViewProxy, + flutterAssetManager: widget.flutterAssetManager, ); } @@ -91,6 +100,8 @@ required this.callbacksHandler, required this.javascriptChannelRegistry, @visibleForTesting this.webViewProxy = const WebViewProxy(), + @visibleForTesting + this.flutterAssetManager = const android_webview.FlutterAssetManager(), }) : assert(creationParams.webSettings?.hasNavigationDelegate != null), super(callbacksHandler) { webView = webViewProxy.createWebView( @@ -134,6 +145,11 @@ /// This should only be changed for testing purposes. final WebViewProxy webViewProxy; + /// Manages access to Flutter assets that are part of the Android App bundle. + /// + /// This should only be changed for testing purposes. + final android_webview.FlutterAssetManager flutterAssetManager; + /// Receives callbacks when content should be downloaded instead. @visibleForTesting late final WebViewAndroidDownloadListener downloadListener = @@ -167,6 +183,28 @@ } @override + Future<void> loadFlutterAsset(String key) async { + final String assetFilePath = + await flutterAssetManager.getAssetFilePathByName(key); + final List<String> pathElements = assetFilePath.split('/'); + final String fileName = pathElements.removeLast(); + final List<String?> paths = + await flutterAssetManager.list(pathElements.join('/')); + + if (!paths.contains(fileName)) { + throw ArgumentError( + 'Asset for key "$key" not found.', + 'key', + ); + } + + return webView.loadUrl( + 'file:///android_asset/$assetFilePath', + <String, String>{}, + ); + } + + @override Future<void> loadUrl( String url, Map<String, String>? headers, @@ -174,6 +212,28 @@ return webView.loadUrl(url, headers ?? <String, String>{}); } + /// When making a POST request, headers are ignored. As a workaround, make + /// the request manually and load the response data using [loadHTMLString]. + @override + Future<void> loadRequest( + WebViewRequest request, + ) async { + if (!request.uri.hasScheme) { + throw ArgumentError('WebViewRequest#uri is required to have a scheme.'); + } + switch (request.method) { + case WebViewRequestMethod.get: + return webView.loadUrl(request.uri.toString(), request.headers); + case WebViewRequestMethod.post: + return webView.postUrl( + request.uri.toString(), request.body ?? Uint8List(0)); + default: + throw UnimplementedError( + 'This version of webview_android_widget currently has no implementation for HTTP method ${request.method.serialize()} in loadRequest.', + ); + } + } + @override Future<String?> currentUrl() => webView.getUrl(); @@ -298,7 +358,20 @@ AutoMediaPlaybackPolicy.always_allow, ); + final Color? backgroundColor = creationParams.backgroundColor; + if (backgroundColor != null) { + webView.setBackgroundColor(backgroundColor); + } + addJavascriptChannels(creationParams.javascriptChannelNames); + + // TODO(BeMacized): Remove once platform implementations + // are able to register themselves (Flutter >=2.8), + // https://github.com/flutter/flutter/issues/94224 + WebViewCookieManagerPlatform.instance ??= WebViewAndroidCookieManager(); + + creationParams.cookies + .forEach(WebViewCookieManagerPlatform.instance!.setCookie); } Future<void> _setHasProgressTracking(bool hasProgressTracking) async {
diff --git a/webview_flutter_android/pigeons/android_webview.dart b/webview_flutter_android/pigeons/android_webview.dart index 9632163..36862f7 100644 --- a/webview_flutter_android/pigeons/android_webview.dart +++ b/webview_flutter_android/pigeons/android_webview.dart
@@ -18,6 +18,14 @@ String? description; } +@HostApi() +abstract class CookieManagerHostApi { + @async + bool clearCookies(); + + void setCookie(String url, String value); +} + @HostApi(dartHostTestHandler: 'TestWebViewHostApi') abstract class WebViewHostApi { void create(int instanceId, bool useHybridComposition); @@ -46,6 +54,12 @@ Map<String, String> headers, ); + void postUrl( + int instanceId, + String url, + Uint8List data, + ); + String getUrl(int instanceId); bool canGoBack(int instanceId); @@ -87,6 +101,8 @@ void setDownloadListener(int instanceId, int listenerInstanceId); void setWebChromeClient(int instanceId, int clientInstanceId); + + void setBackgroundColor(int instanceId, int color); } @HostApi(dartHostTestHandler: 'TestWebSettingsHostApi') @@ -191,6 +207,13 @@ void create(int instanceId, int webViewClientInstanceId); } +@HostApi(dartHostTestHandler: 'TestAssetManagerHostApi') +abstract class FlutterAssetManagerHostApi { + List<String> list(String path); + + String getAssetFilePathByName(String name); +} + @FlutterApi() abstract class WebChromeClientFlutterApi { void dispose(int instanceId);
diff --git a/webview_flutter_android/pubspec.yaml b/webview_flutter_android/pubspec.yaml index 8889469..34bea57 100644 --- a/webview_flutter_android/pubspec.yaml +++ b/webview_flutter_android/pubspec.yaml
@@ -2,7 +2,7 @@ description: A Flutter plugin that provides a WebView widget on Android. repository: https://github.com/flutter/plugins/tree/master/packages/webview_flutter/webview_flutter_android issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+webview%22 -version: 2.4.0 +version: 2.8.0 environment: sdk: ">=2.14.0 <3.0.0" @@ -19,7 +19,7 @@ dependencies: flutter: sdk: flutter - webview_flutter_platform_interface: ^1.5.2 + webview_flutter_platform_interface: ^1.8.0 dev_dependencies: build_runner: ^2.1.4 @@ -30,4 +30,3 @@ mockito: ^5.0.16 pedantic: ^1.10.0 pigeon: 1.0.9 -
diff --git a/webview_flutter_wkwebview/BUILD.gn b/webview_flutter_wkwebview/BUILD.gn index 4b6f053..615d0da 100644 --- a/webview_flutter_wkwebview/BUILD.gn +++ b/webview_flutter_wkwebview/BUILD.gn
@@ -1,4 +1,4 @@ -# This file is generated by package_importer.py for webview_flutter_wkwebview-2.4.0 +# This file is generated by package_importer.py for webview_flutter_wkwebview-2.7.0 import("//build/dart/dart_library.gni") @@ -16,6 +16,7 @@ sources = [ "src/webview_cupertino.dart", + "src/wkwebview_cookie_manager.dart", "webview_flutter_wkwebview.dart", ] }
diff --git a/webview_flutter_wkwebview/CHANGELOG.md b/webview_flutter_wkwebview/CHANGELOG.md index cbf1f04..0aaf4bc 100644 --- a/webview_flutter_wkwebview/CHANGELOG.md +++ b/webview_flutter_wkwebview/CHANGELOG.md
@@ -1,3 +1,18 @@ +## 2.7.0 + +* Adds implementation of the `loadFlutterAsset` method from the platform interface. + +## 2.6.0 + +* Implements new cookie manager for setting cookies and providing initial cookies. + +## 2.5.0 + +* Adds an option to set the background color of the webview. +* Migrates from `analysis_options_legacy.yaml` to `analysis_options.yaml`. +* Integration test fixes. +* Updates to webview_flutter_platform_interface version 1.5.2. + ## 2.4.0 * Implemented new `loadFile` and `loadHtmlString` methods from the platform interface. @@ -16,7 +31,7 @@ ## 2.0.14 -* Update example App so navigation menu loads immediatly but only becomes available when `WebViewController` is available (same behavior as example App in webview_flutter package). +* Update example App so navigation menu loads immediatly but only becomes available when `WebViewController` is available (same behavior as example App in webview_flutter package). ## 2.0.13
diff --git a/webview_flutter_wkwebview/example/integration_test/webview_flutter_test.dart b/webview_flutter_wkwebview/example/integration_test/webview_flutter_test.dart index 17e896c..1e4adb9 100644 --- a/webview_flutter_wkwebview/example/integration_test/webview_flutter_test.dart +++ b/webview_flutter_wkwebview/example/integration_test/webview_flutter_test.dart
@@ -8,6 +8,7 @@ import 'dart:typed_data'; import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter/widgets.dart'; import 'package:flutter_test/flutter_test.dart'; @@ -27,10 +28,6 @@ const String primaryUrl = 'https://flutter.dev/'; const String secondaryUrl = 'https://www.google.com/robots.txt'; - // Set to `false` to include all flaky tests in the test run. See also https://github.com/flutter/flutter/issues/86757. - const bool _skipDueToIssue86757 = false; - - // TODO(bparrishMines): skipped due to https://github.com/flutter/flutter/issues/86757. testWidgets('initialUrl', (WidgetTester tester) async { final Completer<WebViewController> controllerCompleter = Completer<WebViewController>(); @@ -49,9 +46,8 @@ final WebViewController controller = await controllerCompleter.future; final String? currentUrl = await controller.currentUrl(); expect(currentUrl, primaryUrl); - }, skip: _skipDueToIssue86757); + }); - // TODO(bparrishMines): skipped due to https://github.com/flutter/flutter/issues/86757. testWidgets('loadUrl', (WidgetTester tester) async { final Completer<WebViewController> controllerCompleter = Completer<WebViewController>(); @@ -71,7 +67,7 @@ await controller.loadUrl(secondaryUrl); final String? currentUrl = await controller.currentUrl(); expect(currentUrl, secondaryUrl); - }, skip: _skipDueToIssue86757); + }); testWidgets('evaluateJavascript', (WidgetTester tester) async { final Completer<WebViewController> controllerCompleter = @@ -180,91 +176,24 @@ }); testWidgets('resize webview', (WidgetTester tester) async { - final String resizeTest = ''' - <!DOCTYPE html><html> - <head><title>Resize test</title> - <script type="text/javascript"> - function onResize() { - Resize.postMessage("resize"); - } - function onLoad() { - window.onresize = onResize; - } - </script> - </head> - <body onload="onLoad();" bgColor="blue"> - </body> - </html> - '''; - final String resizeTestBase64 = - base64Encode(const Utf8Encoder().convert(resizeTest)); - final Completer<void> resizeCompleter = Completer<void>(); - final Completer<void> pageStarted = Completer<void>(); - final Completer<void> pageLoaded = Completer<void>(); - final Completer<WebViewController> controllerCompleter = - Completer<WebViewController>(); - final GlobalKey key = GlobalKey(); + final Completer<void> buttonTapResizeCompleter = Completer<void>(); + final Completer<void> onPageFinished = Completer<void>(); - final WebView webView = WebView( - key: key, - initialUrl: 'data:text/html;charset=utf-8;base64,$resizeTestBase64', - onWebViewCreated: (WebViewController controller) { - controllerCompleter.complete(controller); + bool resizeButtonTapped = false; + await tester.pumpWidget(ResizableWebView( + onResize: (_) { + if (resizeButtonTapped) { + buttonTapResizeCompleter.complete(); + } }, - javascriptChannels: <JavascriptChannel>{ - JavascriptChannel( - name: 'Resize', - onMessageReceived: (JavascriptMessage message) { - resizeCompleter.complete(true); - }, - ), - }, - onPageStarted: (String url) { - pageStarted.complete(null); - }, - onPageFinished: (String url) { - pageLoaded.complete(null); - }, - javascriptMode: JavascriptMode.unrestricted, - ); + onPageFinished: () => onPageFinished.complete(), + )); + await onPageFinished.future; - await tester.pumpWidget( - Directionality( - textDirection: TextDirection.ltr, - child: Column( - children: <Widget>[ - SizedBox( - width: 200, - height: 200, - child: webView, - ), - ], - ), - ), - ); - - await controllerCompleter.future; - await pageStarted.future; - await pageLoaded.future; - - expect(resizeCompleter.isCompleted, false); - - await tester.pumpWidget( - Directionality( - textDirection: TextDirection.ltr, - child: Column( - children: <Widget>[ - SizedBox( - width: 400, - height: 400, - child: webView, - ), - ], - ), - ), - ); - - await resizeCompleter.future; + resizeButtonTapped = true; + await tester.tap(find.byKey(const ValueKey<String>('resizeButton'))); + await tester.pumpAndSettle(); + expect(buttonTapResizeCompleter.future, completes); }); testWidgets('set custom userAgent', (WidgetTester tester) async { @@ -517,10 +446,10 @@ testWidgets('Video plays inline when allowsInlineMediaPlayback is true', (WidgetTester tester) async { - Completer<WebViewController> controllerCompleter = + final Completer<WebViewController> controllerCompleter = Completer<WebViewController>(); - Completer<void> pageLoaded = Completer<void>(); - Completer<void> videoPlaying = Completer<void>(); + final Completer<void> pageLoaded = Completer<void>(); + final Completer<void> videoPlaying = Completer<void>(); await tester.pumpWidget( Directionality( @@ -551,7 +480,7 @@ ), ), ); - WebViewController controller = await controllerCompleter.future; + final WebViewController controller = await controllerCompleter.future; await pageLoaded.future; // Pump once to trigger the video play. @@ -560,7 +489,7 @@ // Makes sure we get the correct event that indicates the video is actually playing. await videoPlaying.future; - String fullScreen = + final String fullScreen = await controller.runJavascriptReturningResult('isFullScreen();'); expect(fullScreen, _webviewBool(false)); }); @@ -568,10 +497,10 @@ testWidgets( 'Video plays full screen when allowsInlineMediaPlayback is false', (WidgetTester tester) async { - Completer<WebViewController> controllerCompleter = + final Completer<WebViewController> controllerCompleter = Completer<WebViewController>(); - Completer<void> pageLoaded = Completer<void>(); - Completer<void> videoPlaying = Completer<void>(); + final Completer<void> pageLoaded = Completer<void>(); + final Completer<void> videoPlaying = Completer<void>(); await tester.pumpWidget( Directionality( @@ -602,7 +531,7 @@ ), ), ); - WebViewController controller = await controllerCompleter.future; + final WebViewController controller = await controllerCompleter.future; await pageLoaded.future; // Pump once to trigger the video play. @@ -611,7 +540,7 @@ // Makes sure we get the correct event that indicates the video is actually playing. await videoPlaying.future; - String fullScreen = + final String fullScreen = await controller.runJavascriptReturningResult('isFullScreen();'); expect(fullScreen, _webviewBool(true)); }); @@ -789,7 +718,7 @@ }); testWidgets('getTitle', (WidgetTester tester) async { - final String getTitleTest = ''' + const String getTitleTest = ''' <!DOCTYPE html><html> <head><title>Some title</title> </head> @@ -809,6 +738,7 @@ textDirection: TextDirection.ltr, child: WebView( initialUrl: 'data:text/html;charset=utf-8;base64,$getTitleTestBase64', + javascriptMode: JavascriptMode.unrestricted, onWebViewCreated: (WebViewController controller) { controllerCompleter.complete(controller); }, @@ -826,13 +756,19 @@ await pageStarted.future; await pageLoaded.future; + // On at least iOS, it does not appear to be guaranteed that the native + // code has the title when the page load completes. Execute some JavaScript + // before checking the title to ensure that the page has been fully parsed + // and processed. + await controller.runJavascript('1;'); + final String? title = await controller.getTitle(); expect(title, 'Some title'); }); group('Programmatic Scroll', () { testWidgets('setAndGetScrollPosition', (WidgetTester tester) async { - final String scrollTestPage = ''' + const String scrollTestPage = ''' <!DOCTYPE html> <html> <head> @@ -879,7 +815,7 @@ final WebViewController controller = await controllerCompleter.future; await pageLoaded.future; - await tester.pumpAndSettle(Duration(seconds: 3)); + await tester.pumpAndSettle(const Duration(seconds: 3)); int scrollPosX = await controller.getScrollX(); int scrollPosY = await controller.getScrollY(); @@ -909,7 +845,7 @@ }); group('NavigationDelegate', () { - final String blankPage = "<!DOCTYPE html><head></head><body></body></html>"; + const String blankPage = '<!DOCTYPE html><head></head><body></body></html>'; final String blankPageEncoded = 'data:text/html;charset=utf-8;base64,' + base64Encode(const Utf8Encoder().convert(blankPage)); @@ -1005,7 +941,7 @@ testWidgets( 'onWebResourceError only called for main frame', (WidgetTester tester) async { - final String iframeTest = ''' + const String iframeTest = ''' <!DOCTYPE html> <html> <head> @@ -1172,7 +1108,6 @@ expect(currentUrl, primaryUrl); }); - // TODO(bparrishMines): skipped due to https://github.com/flutter/flutter/issues/86757. testWidgets( 'can open new window and go back', (WidgetTester tester) async { @@ -1210,7 +1145,8 @@ await pageLoaded.future; expect(controller.currentUrl(), completion(primaryUrl)); }, - skip: _skipDueToIssue86757, + // Flaky; see https://github.com/flutter/flutter/issues/90976 + skip: true, ); } @@ -1225,13 +1161,77 @@ /// Returns the value used for the HTTP User-Agent: request header in subsequent HTTP requests. Future<String> _getUserAgent(WebViewController controller) async { - return _runJavascriptReturningResult(controller, 'navigator.userAgent;'); + return await controller.runJavascriptReturningResult('navigator.userAgent;'); } -Future<String> _runJavascriptReturningResult( - WebViewController controller, String js) async { - if (defaultTargetPlatform == TargetPlatform.iOS) { - return await controller.runJavascriptReturningResult(js); +class ResizableWebView extends StatefulWidget { + const ResizableWebView( + {required this.onResize, required this.onPageFinished}); + + final JavascriptMessageHandler onResize; + final VoidCallback onPageFinished; + + @override + State<StatefulWidget> createState() => ResizableWebViewState(); +} + +class ResizableWebViewState extends State<ResizableWebView> { + double webViewWidth = 200; + double webViewHeight = 200; + + static const String resizePage = ''' + <!DOCTYPE html><html> + <head><title>Resize test</title> + <script type="text/javascript"> + function onResize() { + Resize.postMessage("resize"); + } + function onLoad() { + window.onresize = onResize; + } + </script> + </head> + <body onload="onLoad();" bgColor="blue"> + </body> + </html> + '''; + + @override + Widget build(BuildContext context) { + final String resizeTestBase64 = + base64Encode(const Utf8Encoder().convert(resizePage)); + return Directionality( + textDirection: TextDirection.ltr, + child: Column( + children: <Widget>[ + SizedBox( + width: webViewWidth, + height: webViewHeight, + child: WebView( + initialUrl: + 'data:text/html;charset=utf-8;base64,$resizeTestBase64', + javascriptChannels: <JavascriptChannel>{ + JavascriptChannel( + name: 'Resize', + onMessageReceived: widget.onResize, + ), + }, + onPageFinished: (_) => widget.onPageFinished(), + javascriptMode: JavascriptMode.unrestricted, + ), + ), + TextButton( + key: const Key('resizeButton'), + onPressed: () { + setState(() { + webViewWidth += 100.0; + webViewHeight += 100.0; + }); + }, + child: const Text('ResizeButton'), + ), + ], + ), + ); } - return jsonDecode(await controller.runJavascriptReturningResult(js)); }
diff --git a/webview_flutter_wkwebview/example/ios/Runner.xcodeproj/project.pbxproj b/webview_flutter_wkwebview/example/ios/Runner.xcodeproj/project.pbxproj index e292b1b..b681c47 100644 --- a/webview_flutter_wkwebview/example/ios/Runner.xcodeproj/project.pbxproj +++ b/webview_flutter_wkwebview/example/ios/Runner.xcodeproj/project.pbxproj
@@ -3,7 +3,7 @@ archiveVersion = 1; classes = { }; - objectVersion = 50; + objectVersion = 46; objects = { /* Begin PBXBuildFile section */ @@ -16,8 +16,9 @@ 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; }; 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; }; 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; }; - AE8C124DC8CA68E4D9B30EAB /* libPods-RunnerTests.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 528CB85D53C983D2C5DAFDC5 /* libPods-RunnerTests.a */; }; + D7587C3652F6906210B3AE88 /* libPods-RunnerTests.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 17781D9462A1AEA7C99F8E45 /* libPods-RunnerTests.a */; }; DAF0E91266956134538CC667 /* libPods-Runner.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 572FFC2B2BA326B420B22679 /* libPods-Runner.a */; }; + E43693B527512C0F00382F85 /* FLTCookieManagerTests.m in Sources */ = {isa = PBXBuildFile; fileRef = E43693B427512C0F00382F85 /* FLTCookieManagerTests.m */; }; F7151F77266057800028CB91 /* FLTWebViewUITests.m in Sources */ = {isa = PBXBuildFile; fileRef = F7151F76266057800028CB91 /* FLTWebViewUITests.m */; }; /* End PBXBuildFile section */ @@ -54,10 +55,11 @@ /* Begin PBXFileReference section */ 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = "<group>"; }; 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = "<group>"; }; + 17781D9462A1AEA7C99F8E45 /* libPods-RunnerTests.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-RunnerTests.a"; sourceTree = BUILT_PRODUCTS_DIR; }; + 2286ACB87EA8CA27E739AD6C /* Pods-RunnerTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.release.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.release.xcconfig"; sourceTree = "<group>"; }; + 39B2BDAA45DC06EAB8A6C4E7 /* Pods-RunnerTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.debug.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.debug.xcconfig"; sourceTree = "<group>"; }; 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = "<group>"; }; - 528CB85D53C983D2C5DAFDC5 /* libPods-RunnerTests.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-RunnerTests.a"; sourceTree = BUILT_PRODUCTS_DIR; }; 572FFC2B2BA326B420B22679 /* libPods-Runner.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-Runner.a"; sourceTree = BUILT_PRODUCTS_DIR; }; - 5C776D27D0DDA247ED5EA72B /* Pods-RunnerTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.release.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.release.xcconfig"; sourceTree = "<group>"; }; 686B4BF82548DBC7000AEA36 /* FLTWKNavigationDelegateTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = FLTWKNavigationDelegateTests.m; sourceTree = "<group>"; }; 68BDCAE923C3F7CB00D9C032 /* RunnerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 68BDCAED23C3F7CB00D9C032 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; }; @@ -74,7 +76,7 @@ 97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = "<group>"; }; 97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; }; B89AA31A64040E4A2F1E0CAF /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = "<group>"; }; - C370F140C3A19241FD8C5E64 /* Pods-RunnerTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.debug.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.debug.xcconfig"; sourceTree = "<group>"; }; + E43693B427512C0F00382F85 /* FLTCookieManagerTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FLTCookieManagerTests.m; sourceTree = "<group>"; }; F7151F74266057800028CB91 /* RunnerUITests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerUITests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; F7151F76266057800028CB91 /* FLTWebViewUITests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FLTWebViewUITests.m; sourceTree = "<group>"; }; F7151F78266057800028CB91 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; }; @@ -86,7 +88,7 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - AE8C124DC8CA68E4D9B30EAB /* libPods-RunnerTests.a in Frameworks */, + D7587C3652F6906210B3AE88 /* libPods-RunnerTests.a in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -112,7 +114,7 @@ isa = PBXGroup; children = ( 572FFC2B2BA326B420B22679 /* libPods-Runner.a */, - 528CB85D53C983D2C5DAFDC5 /* libPods-RunnerTests.a */, + 17781D9462A1AEA7C99F8E45 /* libPods-RunnerTests.a */, ); name = Frameworks; sourceTree = "<group>"; @@ -123,6 +125,7 @@ 686B4BF82548DBC7000AEA36 /* FLTWKNavigationDelegateTests.m */, 68BDCAF523C3F97800D9C032 /* FLTWebViewTests.m */, 68BDCAED23C3F7CB00D9C032 /* Info.plist */, + E43693B427512C0F00382F85 /* FLTCookieManagerTests.m */, ); path = RunnerTests; sourceTree = "<group>"; @@ -190,8 +193,8 @@ children = ( F7A1921261392D1CBDAEC2E8 /* Pods-Runner.debug.xcconfig */, B89AA31A64040E4A2F1E0CAF /* Pods-Runner.release.xcconfig */, - C370F140C3A19241FD8C5E64 /* Pods-RunnerTests.debug.xcconfig */, - 5C776D27D0DDA247ED5EA72B /* Pods-RunnerTests.release.xcconfig */, + 39B2BDAA45DC06EAB8A6C4E7 /* Pods-RunnerTests.debug.xcconfig */, + 2286ACB87EA8CA27E739AD6C /* Pods-RunnerTests.release.xcconfig */, ); path = Pods; sourceTree = "<group>"; @@ -212,7 +215,7 @@ isa = PBXNativeTarget; buildConfigurationList = 68BDCAF223C3F7CB00D9C032 /* Build configuration list for PBXNativeTarget "RunnerTests" */; buildPhases = ( - 0067CEC0658A36CBFF8074E7 /* [CP] Check Pods Manifest.lock */, + AA38EF430495C2FB50F0F114 /* [CP] Check Pods Manifest.lock */, 68BDCAE523C3F7CB00D9C032 /* Sources */, 68BDCAE623C3F7CB00D9C032 /* Frameworks */, 68BDCAE723C3F7CB00D9C032 /* Resources */, @@ -277,13 +280,16 @@ ORGANIZATIONNAME = "The Flutter Authors"; TargetAttributes = { 68BDCAE823C3F7CB00D9C032 = { + DevelopmentTeam = 7624MWN53C; ProvisioningStyle = Automatic; }; 97C146ED1CF9000F007C117D = { CreatedOnToolsVersion = 7.3.1; + DevelopmentTeam = 7624MWN53C; }; F7151F73266057800028CB91 = { CreatedOnToolsVersion = 12.5; + DevelopmentTeam = 7624MWN53C; ProvisioningStyle = Automatic; TestTargetID = 97C146ED1CF9000F007C117D; }; @@ -338,28 +344,6 @@ /* End PBXResourcesBuildPhase section */ /* Begin PBXShellScriptBuildPhase section */ - 0067CEC0658A36CBFF8074E7 /* [CP] Check Pods Manifest.lock */ = { - isa = PBXShellScriptBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - inputFileListPaths = ( - ); - inputPaths = ( - "${PODS_PODFILE_DIR_PATH}/Podfile.lock", - "${PODS_ROOT}/Manifest.lock", - ); - name = "[CP] Check Pods Manifest.lock"; - outputFileListPaths = ( - ); - outputPaths = ( - "$(DERIVED_FILE_DIR)/Pods-RunnerTests-checkManifestLockResult.txt", - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; - showEnvVarsInLog = 0; - }; 3B06AD1E1E4923F5004D2608 /* Thin Binary */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; @@ -410,6 +394,28 @@ shellPath = /bin/sh; shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build\n"; }; + AA38EF430495C2FB50F0F114 /* [CP] Check Pods Manifest.lock */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; + outputFileListPaths = ( + ); + outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-RunnerTests-checkManifestLockResult.txt", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; + showEnvVarsInLog = 0; + }; /* End PBXShellScriptBuildPhase section */ /* Begin PBXSourcesBuildPhase section */ @@ -419,6 +425,7 @@ files = ( 334734012669319100DCC49E /* FLTWebViewTests.m in Sources */, 334734022669319400DCC49E /* FLTWKNavigationDelegateTests.m in Sources */, + E43693B527512C0F00382F85 /* FLTCookieManagerTests.m in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -477,7 +484,7 @@ /* Begin XCBuildConfiguration section */ 68BDCAF023C3F7CB00D9C032 /* Debug */ = { isa = XCBuildConfiguration; - baseConfigurationReference = C370F140C3A19241FD8C5E64 /* Pods-RunnerTests.debug.xcconfig */; + baseConfigurationReference = 39B2BDAA45DC06EAB8A6C4E7 /* Pods-RunnerTests.debug.xcconfig */; buildSettings = { BUNDLE_LOADER = "$(TEST_HOST)"; CODE_SIGN_STYLE = Automatic; @@ -491,7 +498,7 @@ }; 68BDCAF123C3F7CB00D9C032 /* Release */ = { isa = XCBuildConfiguration; - baseConfigurationReference = 5C776D27D0DDA247ED5EA72B /* Pods-RunnerTests.release.xcconfig */; + baseConfigurationReference = 2286ACB87EA8CA27E739AD6C /* Pods-RunnerTests.release.xcconfig */; buildSettings = { BUNDLE_LOADER = "$(TEST_HOST)"; CODE_SIGN_STYLE = Automatic;
diff --git a/webview_flutter_wkwebview/example/ios/RunnerTests/FLTCookieManagerTests.m b/webview_flutter_wkwebview/example/ios/RunnerTests/FLTCookieManagerTests.m new file mode 100644 index 0000000..837c0d8 --- /dev/null +++ b/webview_flutter_wkwebview/example/ios/RunnerTests/FLTCookieManagerTests.m
@@ -0,0 +1,127 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +@import Flutter; +@import XCTest; +@import webview_flutter_wkwebview; +@import webview_flutter_wkwebview.Test; + +// OCMock library doesn't generate a valid modulemap. +#import <OCMock/OCMock.h> + +@interface FLTCookieManagerTests : XCTestCase + +@end + +@implementation FLTCookieManagerTests + +- (void)setUp { + [super setUp]; +} + +- (void)testSetCookieForResultSetsCookieAndReturnsResultOnIOS11 { + if (@available(iOS 11.0, *)) { + // Setup + XCTestExpectation *resultExpectation = [self + expectationWithDescription:@"Should return success result when setting cookie completes."]; + [FLTCookieManager.instance setHttpCookieStore:OCMClassMock(WKHTTPCookieStore.class)]; + NSDictionary *arguments = @{ + @"name" : @"foo", + @"value" : @"bar", + @"domain" : @"flutter.dev", + @"path" : @"/", + }; + NSHTTPCookie *cookie = [NSHTTPCookie cookieWithProperties:@{ + NSHTTPCookieName : arguments[@"name"], + NSHTTPCookieValue : arguments[@"value"], + NSHTTPCookieDomain : arguments[@"domain"], + NSHTTPCookiePath : arguments[@"path"], + }]; + [OCMStub([FLTCookieManager.instance.httpCookieStore setCookie:[OCMArg isEqual:cookie] + completionHandler:[OCMArg any]]) + andDo:^(NSInvocation *invocation) { + void (^setCookieCompletionHandler)(void); + [invocation getArgument:&setCookieCompletionHandler atIndex:3]; + setCookieCompletionHandler(); + }]; + // Run + [[FLTCookieManager instance] + setCookieForResult:^(id _Nullable result) { + XCTAssertNil(result); + [resultExpectation fulfill]; + } + arguments:arguments]; + // Verify + [self waitForExpectationsWithTimeout:30.0 handler:nil]; + } +} + +- (void)testSetCookieForDataSetsCookieOnIOS11 { + if (@available(iOS 11.0, *)) { + // Setup + WKHTTPCookieStore *mockHttpCookieStore = OCMClassMock(WKHTTPCookieStore.class); + [FLTCookieManager.instance setHttpCookieStore:mockHttpCookieStore]; + NSDictionary *cookieData = @{ + @"name" : @"foo", + @"value" : @"bar", + @"domain" : @"flutter.dev", + @"path" : @"/", + }; + // Run + [[FLTCookieManager instance] setCookieForData:cookieData]; + // Verify + NSHTTPCookie *cookie = [NSHTTPCookie cookieWithProperties:@{ + NSHTTPCookieName : cookieData[@"name"], + NSHTTPCookieValue : cookieData[@"value"], + NSHTTPCookieDomain : cookieData[@"domain"], + NSHTTPCookiePath : cookieData[@"path"], + }]; + OCMVerify([mockHttpCookieStore setCookie:[OCMArg isEqual:cookie] + completionHandler:[OCMArg any]]); + } +} + +- (void)testSetCookiesForDataSetsCookiesOnIOS11 { + if (@available(iOS 11.0, *)) { + // Setup + WKHTTPCookieStore *mockHttpCookieStore = OCMClassMock(WKHTTPCookieStore.class); + [FLTCookieManager.instance setHttpCookieStore:mockHttpCookieStore]; + NSArray<NSDictionary *> *cookieDatas = @[ + @{ + @"name" : @"foo1", + @"value" : @"bar1", + @"domain" : @"flutter.dev", + @"path" : @"/", + }, + @{ + @"name" : @"foo2", + @"value" : @"bar2", + @"domain" : @"flutter2.dev", + @"path" : @"/2", + } + ]; + // Run + [[FLTCookieManager instance] setCookiesForData:cookieDatas]; + // Verify + NSHTTPCookie *cookie1 = [NSHTTPCookie cookieWithProperties:@{ + NSHTTPCookieName : cookieDatas[0][@"name"], + NSHTTPCookieValue : cookieDatas[0][@"value"], + NSHTTPCookieDomain : cookieDatas[0][@"domain"], + NSHTTPCookiePath : cookieDatas[0][@"path"], + }]; + + OCMVerify([mockHttpCookieStore setCookie:[OCMArg isEqual:cookie1] + completionHandler:[OCMArg any]]); + NSHTTPCookie *cookie2 = [NSHTTPCookie cookieWithProperties:@{ + NSHTTPCookieName : cookieDatas[1][@"name"], + NSHTTPCookieValue : cookieDatas[1][@"value"], + NSHTTPCookieDomain : cookieDatas[1][@"domain"], + NSHTTPCookiePath : cookieDatas[1][@"path"], + }]; + OCMVerify([mockHttpCookieStore setCookie:[OCMArg isEqual:cookie2] + completionHandler:[OCMArg any]]); + } +} + +@end
diff --git a/webview_flutter_wkwebview/example/ios/RunnerTests/FLTWKNavigationDelegateTests.m b/webview_flutter_wkwebview/example/ios/RunnerTests/FLTWKNavigationDelegateTests.m index a819a9b..d39a9f2 100644 --- a/webview_flutter_wkwebview/example/ios/RunnerTests/FLTWKNavigationDelegateTests.m +++ b/webview_flutter_wkwebview/example/ios/RunnerTests/FLTWKNavigationDelegateTests.m
@@ -5,6 +5,7 @@ @import Flutter; @import XCTest; @import webview_flutter_wkwebview; +@import webview_flutter_wkwebview.Test; // OCMock library doesn't generate a valid modulemap. #import <OCMock/OCMock.h>
diff --git a/webview_flutter_wkwebview/example/ios/RunnerTests/FLTWebViewTests.m b/webview_flutter_wkwebview/example/ios/RunnerTests/FLTWebViewTests.m index a3c314a..976b9c4 100644 --- a/webview_flutter_wkwebview/example/ios/RunnerTests/FLTWebViewTests.m +++ b/webview_flutter_wkwebview/example/ios/RunnerTests/FLTWebViewTests.m
@@ -16,6 +16,8 @@ @property(strong, nonatomic) NSObject<FlutterBinaryMessenger> *mockBinaryMessenger; +@property(strong, nonatomic) FLTCookieManager *mockCookieManager; + @end @implementation FLTWebViewTests @@ -23,6 +25,7 @@ - (void)setUp { [super setUp]; self.mockBinaryMessenger = OCMProtocolMock(@protocol(FlutterBinaryMessenger)); + self.mockCookieManager = OCMClassMock(FLTCookieManager.class); } - (void)testCanInitFLTWebViewController { @@ -35,8 +38,8 @@ } - (void)testCanInitFLTWebViewFactory { - FLTWebViewFactory *factory = - [[FLTWebViewFactory alloc] initWithMessenger:self.mockBinaryMessenger]; + FLTWebViewFactory *factory = [[FLTWebViewFactory alloc] initWithMessenger:self.mockBinaryMessenger + cookieManager:self.mockCookieManager]; XCTAssertNotNil(factory); } @@ -161,7 +164,120 @@ OCMReject([mockWebView loadFileURL:[OCMArg any] allowingReadAccessToURL:[OCMArg any]]); } -- (void)testLoadFileSucceedsWithBaseUrl { +- (void)testLoadFlutterAssetSucceeds { + NSBundle *mockBundle = OCMPartialMock([NSBundle mainBundle]); + NSString *filePath = [FlutterDartProject lookupKeyForAsset:@"assets/file.html"]; + NSURL *url = [NSURL URLWithString:[@"file:///" stringByAppendingString:filePath]]; + [OCMStub([mockBundle URLForResource:[filePath stringByDeletingPathExtension] + withExtension:@"html"]) andReturn:(url)]; + + XCTestExpectation *resultExpectation = + [self expectationWithDescription:@"Should return successful result over the method channel."]; + FLTWebViewController *controller = + [[FLTWebViewController alloc] initWithFrame:CGRectMake(0, 0, 300, 400) + viewIdentifier:1 + arguments:nil + binaryMessenger:self.mockBinaryMessenger]; + FLTWKWebView *mockWebView = OCMClassMock(FLTWKWebView.class); + controller.webView = mockWebView; + [controller onMethodCall:[FlutterMethodCall methodCallWithMethodName:@"loadFlutterAsset" + arguments:@"assets/file.html"] + result:^(id _Nullable result) { + XCTAssertNil(result); + [resultExpectation fulfill]; + }]; + + [self waitForExpectations:@[ resultExpectation ] timeout:1.0]; + OCMVerify([mockWebView loadFileURL:url + allowingReadAccessToURL:[url URLByDeletingLastPathComponent]]); +} + +- (void)testLoadFlutterAssetFailsWithInvalidKey { + NSArray *resultExpectations = @[ + [self expectationWithDescription:@"Should return failed result when argument is nil."], + [self expectationWithDescription: + @"Should return failed result when argument is not of type NSString*."], + [self expectationWithDescription: + @"Should return failed result when argument is an empty string."], + ]; + + FLTWebViewController *controller = + [[FLTWebViewController alloc] initWithFrame:CGRectMake(0, 0, 300, 400) + viewIdentifier:1 + arguments:nil + binaryMessenger:self.mockBinaryMessenger]; + FLTWKWebView *mockWebView = OCMClassMock(FLTWKWebView.class); + controller.webView = mockWebView; + [controller onMethodCall:[FlutterMethodCall methodCallWithMethodName:@"loadFlutterAsset" + arguments:nil] + result:^(id _Nullable result) { + FlutterError *expected = + [FlutterError errorWithCode:@"loadFlutterAsset_invalidKey" + message:@"Supplied asset key is not valid." + details:@"Argument is nil."]; + [FLTWebViewTests assertFlutterError:result withExpected:expected]; + [resultExpectations[0] fulfill]; + }]; + [controller onMethodCall:[FlutterMethodCall methodCallWithMethodName:@"loadFlutterAsset" + arguments:@(10)] + result:^(id _Nullable result) { + FlutterError *expected = + [FlutterError errorWithCode:@"loadFlutterAsset_invalidKey" + message:@"Supplied asset key is not valid." + details:@"Argument is not of type NSString."]; + [FLTWebViewTests assertFlutterError:result withExpected:expected]; + [resultExpectations[1] fulfill]; + }]; + [controller onMethodCall:[FlutterMethodCall methodCallWithMethodName:@"loadFlutterAsset" + arguments:@""] + result:^(id _Nullable result) { + FlutterError *expected = + [FlutterError errorWithCode:@"loadFlutterAsset_invalidKey" + message:@"Supplied asset key is not valid." + details:@"Argument contains an empty string."]; + [FLTWebViewTests assertFlutterError:result withExpected:expected]; + [resultExpectations[2] fulfill]; + }]; + + [self waitForExpectations:resultExpectations timeout:1.0]; + OCMReject([mockWebView loadFileURL:[OCMArg any] allowingReadAccessToURL:[OCMArg any]]); +} + +- (void)testLoadFlutterAssetFailsWithParsingError { + NSBundle *mockBundle = OCMPartialMock([NSBundle mainBundle]); + NSString *filePath = [FlutterDartProject lookupKeyForAsset:@"assets/file.html"]; + [OCMStub([mockBundle URLForResource:[filePath stringByDeletingPathExtension] + withExtension:@"html"]) andReturn:(nil)]; + + XCTestExpectation *resultExpectation = + [self expectationWithDescription:@"Should return failed result over the method channel."]; + FLTWebViewController *controller = + [[FLTWebViewController alloc] initWithFrame:CGRectMake(0, 0, 300, 400) + viewIdentifier:1 + arguments:nil + binaryMessenger:self.mockBinaryMessenger]; + FLTWKWebView *mockWebView = OCMClassMock(FLTWKWebView.class); + controller.webView = mockWebView; + [controller + onMethodCall:[FlutterMethodCall methodCallWithMethodName:@"loadFlutterAsset" + arguments:@"assets/file.html"] + result:^(id _Nullable result) { + FlutterError *expected = [FlutterError + errorWithCode:@"loadFlutterAsset_invalidKey" + message:@"Failed parsing file path for supplied key." + details:[NSString + stringWithFormat: + @"Failed to convert path '%@' into NSURL for key '%@'.", + filePath, @"assets/file.html"]]; + [FLTWebViewTests assertFlutterError:result withExpected:expected]; + [resultExpectation fulfill]; + }]; + + [self waitForExpectations:@[ resultExpectation ] timeout:1.0]; + OCMReject([mockWebView loadFileURL:[OCMArg any] allowingReadAccessToURL:[OCMArg any]]); +} + +- (void)testLoadHtmlStringSucceedsWithBaseUrl { NSURL *baseUrl = [NSURL URLWithString:@"https://flutter.dev"]; XCTestExpectation *resultExpectation = [self expectationWithDescription:@"Should return successful result over the method channel."]; @@ -186,7 +302,7 @@ OCMVerify([mockWebView loadHTMLString:@"some HTML string" baseURL:baseUrl]); } -- (void)testLoadFileSucceedsWithoutBaseUrl { +- (void)testLoadHtmlStringSucceedsWithoutBaseUrl { XCTestExpectation *resultExpectation = [self expectationWithDescription:@"Should return successful result over the method channel."]; FLTWebViewController *controller = @@ -648,7 +764,7 @@ [self waitForExpectationsWithTimeout:30.0 handler:nil]; } -- (void)testOnLoadRequestReturnsErroResultForInvalidRequest { +- (void)testOnLoadRequestReturnsErrorResultForInvalidRequest { // Setup FLTWebViewController *controller = [[FLTWebViewController alloc] initWithFrame:CGRectMake(0, 0, 300, 400) @@ -697,4 +813,21 @@ [self waitForExpectationsWithTimeout:30.0 handler:nil]; } +- (void)testCreateWithFrameShouldSetCookiesOnIOS11 { + if (@available(iOS 11, *)) { + // Setup + FLTWebViewFactory *factory = + [[FLTWebViewFactory alloc] initWithMessenger:self.mockBinaryMessenger + cookieManager:self.mockCookieManager]; + NSArray<NSDictionary *> *cookies = + @[ @{@"name" : @"foo", @"value" : @"bar", @"domain" : @"flutter.dev", @"path" : @"/"} ]; + // Run + [factory createWithFrame:CGRectMake(0, 0, 300, 400) + viewIdentifier:1 + arguments:@{@"cookies" : cookies}]; + // Verify + OCMVerify([_mockCookieManager setCookiesForData:cookies]); + } +} + @end
diff --git a/webview_flutter_wkwebview/example/ios/RunnerUITests/FLTWebViewUITests.m b/webview_flutter_wkwebview/example/ios/RunnerUITests/FLTWebViewUITests.m index d193be7..e43fb97 100644 --- a/webview_flutter_wkwebview/example/ios/RunnerUITests/FLTWebViewUITests.m +++ b/webview_flutter_wkwebview/example/ios/RunnerUITests/FLTWebViewUITests.m
@@ -5,6 +5,25 @@ @import XCTest; @import os.log; +static UIColor* getPixelColorInImage(CGImageRef image, size_t x, size_t y) { + CFDataRef pixelData = CGDataProviderCopyData(CGImageGetDataProvider(image)); + const UInt8* data = CFDataGetBytePtr(pixelData); + + size_t bytesPerRow = CGImageGetBytesPerRow(image); + size_t pixelInfo = (bytesPerRow * y) + (x * 4); // 4 bytes per pixel + + UInt8 red = data[pixelInfo + 0]; + UInt8 green = data[pixelInfo + 1]; + UInt8 blue = data[pixelInfo + 2]; + UInt8 alpha = data[pixelInfo + 3]; + CFRelease(pixelData); + + return [UIColor colorWithRed:red / 255.0f + green:green / 255.0f + blue:blue / 255.0f + alpha:alpha / 255.0f]; +} + @interface FLTWebViewUITests : XCTestCase @property(nonatomic, strong) XCUIApplication* app; @end @@ -18,6 +37,54 @@ [self.app launch]; } +- (void)testTransparentBackground { + XCUIApplication* app = self.app; + XCUIElement* menu = app.buttons[@"Show menu"]; + if (![menu waitForExistenceWithTimeout:30.0]) { + os_log_error(OS_LOG_DEFAULT, "%@", app.debugDescription); + XCTFail(@"Failed due to not able to find menu"); + } + [menu tap]; + + XCUIElement* transparentBackground = app.buttons[@"Transparent background example"]; + if (![transparentBackground waitForExistenceWithTimeout:30.0]) { + os_log_error(OS_LOG_DEFAULT, "%@", app.debugDescription); + XCTFail(@"Failed due to not able to find Transparent background example"); + } + [transparentBackground tap]; + + XCUIElement* transparentBackgroundLoaded = + app.webViews.staticTexts[@"Transparent background test"]; + if (![transparentBackgroundLoaded waitForExistenceWithTimeout:30.0]) { + os_log_error(OS_LOG_DEFAULT, "%@", app.debugDescription); + XCTFail(@"Failed due to not able to find Transparent background test"); + } + + XCUIScreenshot* screenshot = [[XCUIScreen mainScreen] screenshot]; + + UIImage* screenshotImage = screenshot.image; + CGImageRef screenshotCGImage = screenshotImage.CGImage; + UIColor* centerLeftColor = + getPixelColorInImage(screenshotCGImage, 0, CGImageGetHeight(screenshotCGImage) / 2); + UIColor* centerColor = + getPixelColorInImage(screenshotCGImage, CGImageGetWidth(screenshotCGImage) / 2, + CGImageGetHeight(screenshotCGImage) / 2); + + CGColorSpaceRef centerLeftColorSpace = CGColorGetColorSpace(centerLeftColor.CGColor); + // Flutter Colors.green color : 0xFF4CAF50 -> rgba(76, 175, 80, 1) + // https://github.com/flutter/flutter/blob/f4abaa0735eba4dfd8f33f73363911d63931fe03/packages/flutter/lib/src/material/colors.dart#L1208 + // The background color of the webview is : rgba(0, 0, 0, 0.5) + // The expected color is : rgba(38, 87, 40, 1) + CGFloat flutterGreenColorComponents[] = {38.0f / 255.0f, 87.0f / 255.0f, 40.0f / 255.0f, 1.0f}; + CGColorRef flutterGreenColor = CGColorCreate(centerLeftColorSpace, flutterGreenColorComponents); + CGFloat redColorComponents[] = {1.0f, 0.0f, 0.0f, 1.0f}; + CGColorRef redColor = CGColorCreate(centerLeftColorSpace, redColorComponents); + CGColorSpaceRelease(centerLeftColorSpace); + + XCTAssertTrue(CGColorEqualToColor(flutterGreenColor, centerLeftColor.CGColor)); + XCTAssertTrue(CGColorEqualToColor(redColor, centerColor.CGColor)); +} + - (void)testUserAgent { XCUIApplication* app = self.app; XCUIElement* menu = app.buttons[@"Show menu"];
diff --git a/webview_flutter_wkwebview/example/lib/main.dart b/webview_flutter_wkwebview/example/lib/main.dart index 72168ec..d4e0ec4 100644 --- a/webview_flutter_wkwebview/example/lib/main.dart +++ b/webview_flutter_wkwebview/example/lib/main.dart
@@ -19,7 +19,7 @@ import 'web_view.dart'; void main() { - runApp(MaterialApp(home: _WebViewExample())); + runApp(const MaterialApp(home: _WebViewExample())); } const String kNavigationExamplePage = ''' @@ -37,6 +37,27 @@ </html> '''; +const String kTransparentBackgroundPage = ''' +<!DOCTYPE html> +<html> +<head> + <title>Transparent background test</title> +</head> +<style type="text/css"> + body { background: transparent; margin: 0; padding: 0; } + #container { position: relative; margin: 0; padding: 0; width: 100vw; height: 100vh; } + #shape { background: #FF0000; width: 200px; height: 100%; margin: 0; padding: 0; position: absolute; top: 0; bottom: 0; left: calc(50% - 100px); } + p { text-align: center; } +</style> +<body> + <div id="container"> + <p>Transparent background test</p> + <div id="shape"></div> + </div> +</body> +</html> +'''; + const String kLocalFileExamplePage = ''' <!DOCTYPE html> <html lang="en"> @@ -47,8 +68,8 @@ <h1>Local demo page</h1> <p> - This is an example page used to demonstrate how to load a local file or HTML - string using the <a href="https://pub.dev/packages/webview_flutter">Flutter + This is an example page used to demonstrate how to load a local file or HTML + string using the <a href="https://pub.dev/packages/webview_flutter">Flutter webview</a> plugin. </p> @@ -68,8 +89,14 @@ Completer<WebViewController>(); @override + void initState() { + super.initState(); + } + + @override Widget build(BuildContext context) { return Scaffold( + backgroundColor: const Color(0xFF4CAF50), appBar: AppBar( title: const Text('Flutter WebView example'), // This drop down menu demonstrates that Flutter widgets can be shown over the web view. @@ -80,9 +107,9 @@ ), // We're using a Builder here so we have a context that is below the Scaffold // to allow calling Scaffold.of(context) so we can show a snackbar. - body: Builder(builder: (context) { + body: Builder(builder: (BuildContext context) { return WebView( - initialUrl: 'https://flutter.dev', + initialUrl: 'https://flutter.dev/', onWebViewCreated: (WebViewController controller) { _controller.complete(controller); }, @@ -96,6 +123,7 @@ print('allowing navigation to $request'); return NavigationDecision.navigate; }, + backgroundColor: const Color(0x80000000), ); }), floatingActionButton: favoriteButton(), @@ -125,7 +153,7 @@ } Set<JavascriptChannel> _createJavascriptChannels(BuildContext context) { - return { + return <JavascriptChannel>{ JavascriptChannel( name: 'Snackbar', onMessageReceived: (JavascriptMessage message) { @@ -143,13 +171,16 @@ listCache, clearCache, navigationDelegate, + loadFlutterAsset, loadLocalFile, loadHtmlString, doPostRequest, + setCookie, + transparentBackground, } class _SampleMenu extends StatelessWidget { - _SampleMenu(this.controller); + const _SampleMenu(this.controller); final Future<WebViewController> controller; @@ -160,6 +191,7 @@ builder: (BuildContext context, AsyncSnapshot<WebViewController> controller) { return PopupMenuButton<_MenuOptions>( + key: const ValueKey<String>('ShowPopupMenu'), onSelected: (_MenuOptions value) { switch (value) { case _MenuOptions.showUserAgent: @@ -183,6 +215,9 @@ case _MenuOptions.navigationDelegate: _onNavigationDelegateExample(controller.data!, context); break; + case _MenuOptions.loadFlutterAsset: + _onLoadFlutterAssetExample(controller.data!, context); + break; case _MenuOptions.loadLocalFile: _onLoadLocalFileExample(controller.data!, context); break; @@ -192,6 +227,12 @@ case _MenuOptions.doPostRequest: _onDoPostRequest(controller.data!, context); break; + case _MenuOptions.setCookie: + _onSetCookie(controller.data!, context); + break; + case _MenuOptions.transparentBackground: + _onTransparentBackground(controller.data!, context); + break; } }, itemBuilder: (BuildContext context) => <PopupMenuItem<_MenuOptions>>[ @@ -225,6 +266,10 @@ child: Text('Navigation Delegate example'), ), const PopupMenuItem<_MenuOptions>( + value: _MenuOptions.loadFlutterAsset, + child: Text('Load Flutter Asset'), + ), + const PopupMenuItem<_MenuOptions>( value: _MenuOptions.loadHtmlString, child: Text('Load HTML string'), ), @@ -236,13 +281,22 @@ value: _MenuOptions.doPostRequest, child: Text('Post Request'), ), + const PopupMenuItem<_MenuOptions>( + value: _MenuOptions.setCookie, + child: Text('Set Cookie'), + ), + const PopupMenuItem<_MenuOptions>( + key: ValueKey<String>('ShowTransparentBackgroundExample'), + value: _MenuOptions.transparentBackground, + child: Text('Transparent background example'), + ), ], ); }, ); } - void _onShowUserAgent( + Future<void> _onShowUserAgent( WebViewController controller, BuildContext context) async { // Send a message with the user agent string to the Snackbar JavaScript channel we registered // with the WebView. @@ -250,7 +304,7 @@ 'Snackbar.postMessage("User Agent: " + navigator.userAgent);'); } - void _onListCookies( + Future<void> _onListCookies( WebViewController controller, BuildContext context) async { final String cookies = await controller.runJavascriptReturningResult('document.cookie'); @@ -266,7 +320,8 @@ )); } - void _onAddToCache(WebViewController controller, BuildContext context) async { + Future<void> _onAddToCache( + WebViewController controller, BuildContext context) async { await controller.runJavascript( 'caches.open("test_caches_entry"); localStorage["test_localStorage"] = "dummy_entry";'); ScaffoldMessenger.of(context).showSnackBar(const SnackBar( @@ -274,20 +329,22 @@ )); } - void _onListCache(WebViewController controller, BuildContext context) async { + Future<void> _onListCache( + WebViewController controller, BuildContext context) async { await controller.runJavascript('caches.keys()' '.then((cacheKeys) => JSON.stringify({"cacheKeys" : cacheKeys, "localStorage" : localStorage}))' '.then((caches) => Snackbar.postMessage(caches))'); } - void _onClearCache(WebViewController controller, BuildContext context) async { + Future<void> _onClearCache( + WebViewController controller, BuildContext context) async { await controller.clearCache(); ScaffoldMessenger.of(context).showSnackBar(const SnackBar( - content: Text("Cache cleared."), + content: Text('Cache cleared.'), )); } - void _onClearCookies( + Future<void> _onClearCookies( WebViewController controller, BuildContext context) async { final bool hadCookies = await WebView.platform.clearCookies(); String message = 'There were cookies. Now, they are gone!'; @@ -299,36 +356,50 @@ )); } - void _onNavigationDelegateExample( + Future<void> _onNavigationDelegateExample( WebViewController controller, BuildContext context) async { final String contentBase64 = base64Encode(const Utf8Encoder().convert(kNavigationExamplePage)); await controller.loadUrl('data:text/html;base64,$contentBase64'); } - void _onLoadLocalFileExample( + Future<void> _onLoadFlutterAssetExample( WebViewController controller, BuildContext context) async { - String pathToIndex = await _prepareLocalFile(); + await controller.loadFlutterAsset('assets/www/index.html'); + } + + Future<void> _onLoadLocalFileExample( + WebViewController controller, BuildContext context) async { + final String pathToIndex = await _prepareLocalFile(); await controller.loadFile(pathToIndex); } - void _onLoadHtmlStringExample( + Future<void> _onLoadHtmlStringExample( WebViewController controller, BuildContext context) async { await controller.loadHtmlString(kLocalFileExamplePage); } - void _onDoPostRequest( + Future<void> _onDoPostRequest( WebViewController controller, BuildContext context) async { - WebViewRequest request = WebViewRequest( + final WebViewRequest request = WebViewRequest( uri: Uri.parse('https://httpbin.org/post'), method: WebViewRequestMethod.post, - headers: {'foo': 'bar', 'Content-Type': 'text/plain'}, + headers: <String, String>{'foo': 'bar', 'Content-Type': 'text/plain'}, body: Uint8List.fromList('Test Body'.codeUnits), ); await controller.loadRequest(request); } + Future<void> _onSetCookie( + WebViewController controller, BuildContext context) async { + await WebViewCookieManager.instance.setCookie( + const WebViewCookie( + name: 'foo', value: 'bar', domain: 'httpbin.org', path: '/anything'), + ); + await controller.loadUrl('https://httpbin.org/anything'); + } + Widget _getCookieList(String cookies) { if (cookies == null || cookies == '""') { return Container(); @@ -343,9 +414,14 @@ ); } + Future<void> _onTransparentBackground( + WebViewController controller, BuildContext context) async { + await controller.loadHtmlString(kTransparentBackgroundPage); + } + static Future<String> _prepareLocalFile() async { final String tmpDir = (await getTemporaryDirectory()).path; - File indexFile = File('$tmpDir/www/index.html'); + final File indexFile = File('$tmpDir/www/index.html'); await Directory('$tmpDir/www').create(recursive: true); await indexFile.writeAsString(kLocalFileExamplePage); @@ -382,7 +458,7 @@ } else { // ignore: deprecated_member_use Scaffold.of(context).showSnackBar( - const SnackBar(content: Text("No back history item")), + const SnackBar(content: Text('No back history item')), ); return; } @@ -399,7 +475,7 @@ // ignore: deprecated_member_use Scaffold.of(context).showSnackBar( const SnackBar( - content: Text("No forward history item")), + content: Text('No forward history item')), ); return; } @@ -421,4 +497,4 @@ } /// Callback type for handling messages sent from JavaScript running in a web view. -typedef void JavascriptMessageHandler(JavascriptMessage message); +typedef JavascriptMessageHandler = void Function(JavascriptMessage message);
diff --git a/webview_flutter_wkwebview/example/lib/navigation_request.dart b/webview_flutter_wkwebview/example/lib/navigation_request.dart index c1ff8dc..2f6d7c9 100644 --- a/webview_flutter_wkwebview/example/lib/navigation_request.dart +++ b/webview_flutter_wkwebview/example/lib/navigation_request.dart
@@ -14,6 +14,6 @@ @override String toString() { - return '$runtimeType(url: $url, isForMainFrame: $isForMainFrame)'; + return 'NavigationRequest(url: $url, isForMainFrame: $isForMainFrame)'; } }
diff --git a/webview_flutter_wkwebview/example/lib/web_view.dart b/webview_flutter_wkwebview/example/lib/web_view.dart index ab4b773..4d479f9 100644 --- a/webview_flutter_wkwebview/example/lib/web_view.dart +++ b/webview_flutter_wkwebview/example/lib/web_view.dart
@@ -3,6 +3,7 @@ // found in the LICENSE file. import 'dart:async'; +import 'dart:io'; import 'package:flutter/foundation.dart'; import 'package:flutter/gestures.dart'; @@ -15,7 +16,7 @@ /// Optional callback invoked when a web view is first created. [controller] is /// the [WebViewController] for the created web view. -typedef void WebViewCreatedCallback(WebViewController controller); +typedef WebViewCreatedCallback = void Function(WebViewController controller); /// Decides how to handle a specific navigation request. /// @@ -23,20 +24,20 @@ /// `navigation` should be handled. /// /// See also: [WebView.navigationDelegate]. -typedef FutureOr<NavigationDecision> NavigationDelegate( +typedef NavigationDelegate = FutureOr<NavigationDecision> Function( NavigationRequest navigation); /// Signature for when a [WebView] has started loading a page. -typedef void PageStartedCallback(String url); +typedef PageStartedCallback = void Function(String url); /// Signature for when a [WebView] has finished loading a page. -typedef void PageFinishedCallback(String url); +typedef PageFinishedCallback = void Function(String url); /// Signature for when a [WebView] is loading a page. -typedef void PageLoadingCallback(int progress); +typedef PageLoadingCallback = void Function(int progress); /// Signature for when a [WebView] has failed to load a resource. -typedef void WebResourceErrorCallback(WebResourceError error); +typedef WebResourceErrorCallback = void Function(WebResourceError error); /// A web view widget for showing html content. /// @@ -54,6 +55,7 @@ Key? key, this.onWebViewCreated, this.initialUrl, + this.initialCookies = const <WebViewCookie>[], this.javascriptMode = JavascriptMode.disabled, this.javascriptChannels, this.navigationDelegate, @@ -69,6 +71,7 @@ this.initialMediaPlaybackPolicy = AutoMediaPlaybackPolicy.require_user_action_for_all_media_types, this.allowsInlineMediaPlayback = false, + this.backgroundColor, }) : assert(javascriptMode != null), assert(initialMediaPlaybackPolicy != null), assert(allowsInlineMediaPlayback != null), @@ -94,6 +97,9 @@ /// The initial URL to load. final String? initialUrl; + /// The initial cookies to set. + final List<WebViewCookie> initialCookies; + /// Whether JavaScript execution is enabled. final JavascriptMode javascriptMode; @@ -227,6 +233,12 @@ /// The default policy is [AutoMediaPlaybackPolicy.require_user_action_for_all_media_types]. final AutoMediaPlaybackPolicy initialMediaPlaybackPolicy; + /// The background color of the [WebView]. + /// + /// When `null` the platform's webview default background color is used. By + /// default [backgroundColor] is `null`. + final Color? backgroundColor; + @override _WebViewState createState() => _WebViewState(); } @@ -259,7 +271,7 @@ context: context, onWebViewPlatformCreated: (WebViewPlatformController? webViewPlatformController) { - WebViewController controller = WebViewController._( + final WebViewController controller = WebViewController._( widget, webViewPlatformController!, _javascriptChannelRegistry, @@ -278,6 +290,8 @@ _javascriptChannelRegistry.channels.keys.toSet(), autoMediaPlaybackPolicy: widget.initialMediaPlaybackPolicy, userAgent: widget.userAgent, + cookies: widget.initialCookies, + backgroundColor: widget.backgroundColor, ), javascriptChannelRegistry: _javascriptChannelRegistry, ); @@ -305,6 +319,14 @@ WebView _widget; + /// Loads the Flutter asset specified in the pubspec.yaml file. + /// + /// Throws an ArgumentError if [key] is not part of the specified assets + /// in the pubspec.yaml file. + Future<void> loadFlutterAsset(String key) { + return _webViewPlatformController.loadFlutterAsset(key); + } + /// Loads the file located on the specified [absoluteFilePath]. /// /// The [absoluteFilePath] parameter should contain the absolute path to the @@ -547,7 +569,7 @@ bool? hasNavigationDelegate; bool? hasProgressTracking; bool? debuggingEnabled; - WebSetting<String?> userAgent = WebSetting.absent(); + WebSetting<String?> userAgent = const WebSetting<String?>.absent(); if (currentValue.javascriptMode != newValue.javascriptMode) { javascriptMode = newValue.javascriptMode; } @@ -645,9 +667,28 @@ } } + @override void onWebResourceError(WebResourceError error) { if (_webView.onWebResourceError != null) { _webView.onWebResourceError!(error); } } } + +/// App-facing cookie manager that exposes the correct platform implementation. +class WebViewCookieManager extends WebViewCookieManagerPlatform { + WebViewCookieManager._(); + + /// Returns an instance of the cookie manager for the current platform. + static WebViewCookieManagerPlatform get instance { + if (WebViewCookieManagerPlatform.instance == null) { + if (Platform.isIOS) { + WebViewCookieManagerPlatform.instance = WKWebViewCookieManager(); + } else { + throw AssertionError( + 'This platform is currently unsupported for webview_flutter_wkwebview.'); + } + } + return WebViewCookieManagerPlatform.instance!; + } +}
diff --git a/webview_flutter_wkwebview/example/pubspec.yaml b/webview_flutter_wkwebview/example/pubspec.yaml index c8001c8..b8c2464 100644 --- a/webview_flutter_wkwebview/example/pubspec.yaml +++ b/webview_flutter_wkwebview/example/pubspec.yaml
@@ -10,7 +10,7 @@ sdk: flutter path_provider: ^2.0.6 - + webview_flutter_wkwebview: # When depending on this package from a real application you should use: # webview_flutter: ^x.y.z @@ -21,10 +21,10 @@ dev_dependencies: espresso: ^0.1.0+2 - flutter_test: - sdk: flutter flutter_driver: sdk: flutter + flutter_test: + sdk: flutter integration_test: sdk: flutter pedantic: ^1.10.0 @@ -34,4 +34,5 @@ assets: - assets/sample_audio.ogg - assets/sample_video.mp4 - \ No newline at end of file + - assets/www/index.html + - assets/www/styles/style.css
diff --git a/webview_flutter_wkwebview/ios/Classes/FLTCookieManager.h b/webview_flutter_wkwebview/ios/Classes/FLTCookieManager.h index 8fe3318..dc5b8f5 100644 --- a/webview_flutter_wkwebview/ios/Classes/FLTCookieManager.h +++ b/webview_flutter_wkwebview/ios/Classes/FLTCookieManager.h
@@ -9,6 +9,12 @@ @interface FLTCookieManager : NSObject <FlutterPlugin> ++ (FLTCookieManager*)instance; + +- (void)setCookiesForData:(NSArray<NSDictionary*>*)cookies; + +- (void)setCookieForData:(NSDictionary*)cookie; + @end NS_ASSUME_NONNULL_END
diff --git a/webview_flutter_wkwebview/ios/Classes/FLTCookieManager.m b/webview_flutter_wkwebview/ios/Classes/FLTCookieManager.m index f4783ff..39976d1 100644 --- a/webview_flutter_wkwebview/ios/Classes/FLTCookieManager.m +++ b/webview_flutter_wkwebview/ios/Classes/FLTCookieManager.m
@@ -3,22 +3,32 @@ // found in the LICENSE file. #import "FLTCookieManager.h" +#import "FLTCookieManager_Test.h" @implementation FLTCookieManager { + WKHTTPCookieStore *_httpCookieStore API_AVAILABLE(macos(10.13), ios(11.0)); +} + ++ (FLTCookieManager *)instance { + static FLTCookieManager *instance = nil; + if (instance == nil) { + instance = [[FLTCookieManager alloc] init]; + } + return instance; } + (void)registerWithRegistrar:(NSObject<FlutterPluginRegistrar> *)registrar { - FLTCookieManager *instance = [[FLTCookieManager alloc] init]; - FlutterMethodChannel *channel = [FlutterMethodChannel methodChannelWithName:@"plugins.flutter.io/cookie_manager" binaryMessenger:[registrar messenger]]; - [registrar addMethodCallDelegate:instance channel:channel]; + [registrar addMethodCallDelegate:[self instance] channel:channel]; } - (void)handleMethodCall:(FlutterMethodCall *)call result:(FlutterResult)result { if ([[call method] isEqualToString:@"clearCookies"]) { [self clearCookies:result]; + } else if ([[call method] isEqualToString:@"setCookie"]) { + [self setCookieForResult:result arguments:[call arguments]]; } else { result(FlutterMethodNotImplemented); } @@ -46,4 +56,50 @@ } } +- (void)setCookiesForData:(NSArray<NSDictionary *> *)cookies { + for (id cookie in cookies) { + [self setCookieForData:cookie]; + } +} + +- (void)setCookieForData:(NSDictionary *)cookieData { + if (@available(iOS 11.0, *)) { + if (!_httpCookieStore) { + _httpCookieStore = [[WKWebsiteDataStore defaultDataStore] httpCookieStore]; + } + NSHTTPCookie *cookie = [NSHTTPCookie cookieWithProperties:@{ + NSHTTPCookieName : cookieData[@"name"], + NSHTTPCookieValue : cookieData[@"value"], + NSHTTPCookieDomain : cookieData[@"domain"], + NSHTTPCookiePath : cookieData[@"path"], + }]; + [_httpCookieStore setCookie:cookie + completionHandler:^{ + }]; + } else { + NSLog(@"Setting cookies is not supported for Flutter WebViews prior to iOS 11."); + } +} + +- (void)setCookieForResult:(FlutterResult)result arguments:(NSDictionary *)arguments { + if (@available(iOS 11.0, *)) { + if (!_httpCookieStore) { + _httpCookieStore = [[WKWebsiteDataStore defaultDataStore] httpCookieStore]; + } + NSHTTPCookie *cookie = [NSHTTPCookie cookieWithProperties:@{ + NSHTTPCookieName : arguments[@"name"], + NSHTTPCookieValue : arguments[@"value"], + NSHTTPCookieDomain : arguments[@"domain"], + NSHTTPCookiePath : arguments[@"path"], + }]; + [_httpCookieStore setCookie:cookie + completionHandler:^{ + result(nil); + }]; + } else { + NSLog(@"Setting cookies is not supported for Flutter WebViews prior to iOS 11."); + result(nil); + } +} + @end
diff --git a/webview_flutter_wkwebview/ios/Classes/FLTCookieManager_Test.h b/webview_flutter_wkwebview/ios/Classes/FLTCookieManager_Test.h new file mode 100644 index 0000000..fecec49 --- /dev/null +++ b/webview_flutter_wkwebview/ios/Classes/FLTCookieManager_Test.h
@@ -0,0 +1,20 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +// This header is available in the Test module. Import via "@import webview_flutter_wkwebview.Test;" + +#import <webview_flutter_wkwebview/FLTCookieManager.h> + +NS_ASSUME_NONNULL_BEGIN + +@interface FLTCookieManager () + +@property(nonatomic, strong) + WKHTTPCookieStore *httpCookieStore API_AVAILABLE(macos(10.13), ios(11.0)); + +- (void)setCookieForResult:(FlutterResult)result arguments:(NSDictionary *)arguments; + +@end + +NS_ASSUME_NONNULL_END
diff --git a/webview_flutter_wkwebview/ios/Classes/FLTWebViewFlutterPlugin.m b/webview_flutter_wkwebview/ios/Classes/FLTWebViewFlutterPlugin.m index 9f01416..0d04d6c 100644 --- a/webview_flutter_wkwebview/ios/Classes/FLTWebViewFlutterPlugin.m +++ b/webview_flutter_wkwebview/ios/Classes/FLTWebViewFlutterPlugin.m
@@ -9,10 +9,11 @@ @implementation FLTWebViewFlutterPlugin + (void)registerWithRegistrar:(NSObject<FlutterPluginRegistrar>*)registrar { - FLTWebViewFactory* webviewFactory = - [[FLTWebViewFactory alloc] initWithMessenger:registrar.messenger]; - [registrar registerViewFactory:webviewFactory withId:@"plugins.flutter.io/webview"]; [FLTCookieManager registerWithRegistrar:registrar]; + FLTWebViewFactory* webviewFactory = + [[FLTWebViewFactory alloc] initWithMessenger:registrar.messenger + cookieManager:[FLTCookieManager instance]]; + [registrar registerViewFactory:webviewFactory withId:@"plugins.flutter.io/webview"]; } @end
diff --git a/webview_flutter_wkwebview/ios/Classes/FlutterWebView.h b/webview_flutter_wkwebview/ios/Classes/FlutterWebView.h index 6d8e463..145a772 100644 --- a/webview_flutter_wkwebview/ios/Classes/FlutterWebView.h +++ b/webview_flutter_wkwebview/ios/Classes/FlutterWebView.h
@@ -2,6 +2,7 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. +#import <FLTCookieManager.h> #import <Flutter/Flutter.h> #import <WebKit/WebKit.h> @@ -31,7 +32,8 @@ @end @interface FLTWebViewFactory : NSObject <FlutterPlatformViewFactory> -- (instancetype)initWithMessenger:(NSObject<FlutterBinaryMessenger>*)messenger; +- (instancetype)initWithMessenger:(NSObject<FlutterBinaryMessenger>*)messenger + cookieManager:(FLTCookieManager*)cookieManager; @end NS_ASSUME_NONNULL_END
diff --git a/webview_flutter_wkwebview/ios/Classes/FlutterWebView.m b/webview_flutter_wkwebview/ios/Classes/FlutterWebView.m index 351d1ae..3f3b9d9 100644 --- a/webview_flutter_wkwebview/ios/Classes/FlutterWebView.m +++ b/webview_flutter_wkwebview/ios/Classes/FlutterWebView.m
@@ -10,12 +10,15 @@ @implementation FLTWebViewFactory { NSObject<FlutterBinaryMessenger>* _messenger; + FLTCookieManager* _cookieManager; } -- (instancetype)initWithMessenger:(NSObject<FlutterBinaryMessenger>*)messenger { +- (instancetype)initWithMessenger:(NSObject<FlutterBinaryMessenger>*)messenger + cookieManager:(FLTCookieManager*)cookieManager { self = [super init]; if (self) { _messenger = messenger; + _cookieManager = cookieManager; } return self; } @@ -27,6 +30,10 @@ - (NSObject<FlutterPlatformView>*)createWithFrame:(CGRect)frame viewIdentifier:(int64_t)viewId arguments:(id _Nullable)args { + if (@available(iOS 11.0, *)) { + [_cookieManager setCookiesForData:args[@"cookies"]]; + } + FLTWebViewController* webviewController = [[FLTWebViewController alloc] initWithFrame:frame viewIdentifier:viewId arguments:args @@ -96,6 +103,20 @@ inConfiguration:configuration]; _webView = [[FLTWKWebView alloc] initWithFrame:frame configuration:configuration]; + + // Background color + NSNumber* backgroundColorNSNumber = args[@"backgroundColor"]; + if ([backgroundColorNSNumber isKindOfClass:[NSNumber class]]) { + int backgroundColorInt = [backgroundColorNSNumber intValue]; + UIColor* backgroundColor = [UIColor colorWithRed:(backgroundColorInt >> 16 & 0xff) / 255.0 + green:(backgroundColorInt >> 8 & 0xff) / 255.0 + blue:(backgroundColorInt & 0xff) / 255.0 + alpha:(backgroundColorInt >> 24 & 0xff) / 255.0]; + _webView.opaque = NO; + _webView.backgroundColor = UIColor.clearColor; + _webView.scrollView.backgroundColor = backgroundColor; + } + _navigationDelegate = [[FLTWKNavigationDelegate alloc] initWithChannel:_channel]; _webView.UIDelegate = self; _webView.navigationDelegate = _navigationDelegate; @@ -142,6 +163,8 @@ [self onUpdateSettings:call result:result]; } else if ([[call method] isEqualToString:@"loadFile"]) { [self onLoadFile:call result:result]; + } else if ([[call method] isEqualToString:@"loadFlutterAsset"]) { + [self onLoadFlutterAsset:call result:result]; } else if ([[call method] isEqualToString:@"loadHtmlString"]) { [self onLoadHtmlString:call result:result]; } else if ([[call method] isEqualToString:@"loadUrl"]) { @@ -223,6 +246,34 @@ result(nil); } +- (void)onLoadFlutterAsset:(FlutterMethodCall*)call result:(FlutterResult)result { + NSString* error = nil; + if (![FLTWebViewController isValidStringArgument:[call arguments] withErrorMessage:&error]) { + result([FlutterError errorWithCode:@"loadFlutterAsset_invalidKey" + message:@"Supplied asset key is not valid." + details:error]); + return; + } + + NSString* assetKey = [call arguments]; + NSString* assetFilePath = [FlutterDartProject lookupKeyForAsset:assetKey]; + NSURL* url = [[NSBundle mainBundle] URLForResource:[assetFilePath stringByDeletingPathExtension] + withExtension:assetFilePath.pathExtension]; + + if (!url) { + result([FlutterError + errorWithCode:@"loadFlutterAsset_invalidKey" + message:@"Failed parsing file path for supplied key." + details:[NSString + stringWithFormat:@"Failed to convert path '%@' into NSURL for key '%@'.", + assetFilePath, assetKey]]); + return; + } + + [_webView loadFileURL:url allowingReadAccessToURL:[url URLByDeletingLastPathComponent]]; + result(nil); +} + - (void)onLoadHtmlString:(FlutterMethodCall*)call result:(FlutterResult)result { NSDictionary* arguments = [call arguments]; if (![arguments isKindOfClass:NSDictionary.class]) {
diff --git a/webview_flutter_wkwebview/ios/Classes/FlutterWebView.modulemap b/webview_flutter_wkwebview/ios/Classes/FlutterWebView.modulemap index fa51430..0965075 100644 --- a/webview_flutter_wkwebview/ios/Classes/FlutterWebView.modulemap +++ b/webview_flutter_wkwebview/ios/Classes/FlutterWebView.modulemap
@@ -6,5 +6,6 @@ explicit module Test { header "FlutterWebView_Test.h" + header "FLTCookieManager_Test.h" } }
diff --git a/webview_flutter_wkwebview/lib/src/webview_cupertino.dart b/webview_flutter_wkwebview/lib/src/webview_cupertino.dart index 05b79d0..cd00d9a 100644 --- a/webview_flutter_wkwebview/lib/src/webview_cupertino.dart +++ b/webview_flutter_wkwebview/lib/src/webview_cupertino.dart
@@ -45,5 +45,11 @@ } @override - Future<bool> clearCookies() => MethodChannelWebViewPlatform.clearCookies(); + Future<bool> clearCookies() { + if (WebViewCookieManagerPlatform.instance == null) { + throw Exception( + 'Could not clear cookies as no implementation for WebViewCookieManagerPlatform has been registered.'); + } + return WebViewCookieManagerPlatform.instance!.clearCookies(); + } }
diff --git a/webview_flutter_wkwebview/lib/src/wkwebview_cookie_manager.dart b/webview_flutter_wkwebview/lib/src/wkwebview_cookie_manager.dart new file mode 100644 index 0000000..d460ce0 --- /dev/null +++ b/webview_flutter_wkwebview/lib/src/wkwebview_cookie_manager.dart
@@ -0,0 +1,30 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:webview_flutter_platform_interface/webview_flutter_platform_interface.dart'; + +/// Handles all cookie operations for the current platform. +class WKWebViewCookieManager extends WebViewCookieManagerPlatform { + @override + Future<bool> clearCookies() => MethodChannelWebViewPlatform.clearCookies(); + + @override + Future<void> setCookie(WebViewCookie cookie) { + if (!_isValidPath(cookie.path)) { + throw ArgumentError( + 'The path property for the provided cookie was not given a legal value.'); + } + return MethodChannelWebViewPlatform.setCookie(cookie); + } + + bool _isValidPath(String path) { + // Permitted ranges based on RFC6265bis: https://datatracker.ietf.org/doc/html/draft-ietf-httpbis-rfc6265bis-02#section-4.1.1 + for (final int char in path.codeUnits) { + if ((char < 0x20 || char > 0x3A) && (char < 0x3C || char > 0x7E)) { + return false; + } + } + return true; + } +}
diff --git a/webview_flutter_wkwebview/lib/webview_flutter_wkwebview.dart b/webview_flutter_wkwebview/lib/webview_flutter_wkwebview.dart index bbec415..f647ab3 100644 --- a/webview_flutter_wkwebview/lib/webview_flutter_wkwebview.dart +++ b/webview_flutter_wkwebview/lib/webview_flutter_wkwebview.dart
@@ -3,3 +3,4 @@ // found in the LICENSE file. export 'src/webview_cupertino.dart'; +export 'src/wkwebview_cookie_manager.dart';
diff --git a/webview_flutter_wkwebview/pubspec.yaml b/webview_flutter_wkwebview/pubspec.yaml index 466c1a2..08e98b1 100644 --- a/webview_flutter_wkwebview/pubspec.yaml +++ b/webview_flutter_wkwebview/pubspec.yaml
@@ -2,7 +2,7 @@ description: A Flutter plugin that provides a WebView widget based on Apple's WKWebView control. repository: https://github.com/flutter/plugins/tree/master/packages/webview_flutter/webview_flutter_wkwebview issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+webview%22 -version: 2.4.0 +version: 2.7.0 environment: sdk: ">=2.14.0 <3.0.0" @@ -18,11 +18,11 @@ dependencies: flutter: sdk: flutter - webview_flutter_platform_interface: ^1.3.0 + webview_flutter_platform_interface: ^1.8.0 dev_dependencies: flutter_driver: sdk: flutter flutter_test: sdk: flutter - pedantic: ^1.10.0 \ No newline at end of file + pedantic: ^1.10.0