Compare commits

...

15 Commits

Author SHA1 Message Date
Kannan J c7202c0db5
Bump readme (#12305) 2025-08-24 18:21:52 +05:30
Eric Anderson 028afbe352
xds: Implement equals in WRRLBConfig
Just an is a8de9f0, lack of equals causes cluster_resolver to consider every update a different configuration and restart itself.

Handling NaN should really be prevented with validation, but it looks like that
would lead to yak shaving at the moment.

b/435208946
2025-08-22 08:07:51 -07:00
John Cormie afdbecb235
binder: Move BinderTransport's inner classes to the top level (#12303)
BinderTransport.java was getting too long and deeply nested.

This is a pure refactor with no behavior changes.
2025-08-21 16:08:58 -07:00
MV Shiva 2039266ebc
xds: xdsClient caches transient error for new watchers (#12262) 2025-08-19 21:41:52 +05:30
Jiri Kaplan 43bef65cf9 netty: Support BCJSSE provider in GrpcSslContexts 2025-08-19 07:17:57 -07:00
Eric Anderson 437e03dc98
xds: Avoid PriorityLb re-enabling timer on duplicate CONNECTING (#12289)
Since c4256add4 we no longer fabricate a TRANSIENT_FAILURE update from
children. However, previously that would have set
seenReadyOrIdleSinceTransientFailure = false and prevented future timer
creation. If a LB policy gives extraneous updates with state CONNECTING,
then it was possible to re-create failOverTimer which would then wait
the 10 seconds for the child to finish CONNECTING. We only want to give
the child one opportunity after transitioning out of READY/IDLE.

https://github.com/grpc/proposal/pull/509
2025-08-19 12:53:47 +05:30
Eric Anderson 6462ef9a11 netty: Count sent RST_STREAMs against limit
Http2RstCounterEncoder has to be constructed before
NettyServerHandler/Http2ConnectionHandler so it must be static. Thus the
code/counters were moved into RstStreamCounter which then can be
constructed earlier and shared.

This depends on Netty 4.1.124 for a bug fix to actually call the
encoder:
be53dc3c9a
2025-08-18 07:23:34 -07:00
Eric Anderson 95d16d85c8 Upgrade to Netty 4.1.124.Final
This implicitly disables NettyAdaptiveCumulator (#11284), which can have a
performance impact. We delayed upgrading Netty to give time to rework
the optimization, but we've gone too long already without upgrading
which causes problems for vulnerability tracking.
2025-08-13 14:23:14 -07:00
Sangamesh f50726d32e
android: Clean up android lint and other warnings (#12143)
Worked on clearing the lint warnings (OldTargetApi, ObsoleteSdkInt,
InlinedApi, NewApi)

Fixes #12142
2025-08-11 15:18:01 -07:00
Eric Anderson 06707f7c38 xds: Use a different log name for XdsClientImpl and ControlPlaneClient
Seems like a good time to stop hating ourselves, as that seems to be the
only reason to use the same string.
2025-08-08 14:23:43 -07:00
John Cormie efcdebb904
Introduce a NameResolver for Android's `intent:` URIs (#12248)
Let grpc-binder clients find on-device services by [implicit Intent](https://developer.android.com/guide/components/intents-filters#Types) target URI, lifting the need to hard code a server's package name.
2025-08-07 08:38:44 -07:00
Eric Anderson f30964ab82
Bump versions of dependencies (#12252)
Notably, protobuf to 3.25.8, opentelemetry to 1.52.0. Protobuf in Bazel
has 25.5 in the BCR and it seems better to align the WORKSPACE
with that version. But we can't actually use 25.5 in BCR because it is
incompatible with Bazel 7.
2025-08-06 11:01:45 -07:00
MV Shiva 7040417eee
stub: use the closedTrailers in StatusException (#12259) 2025-08-06 12:24:33 +05:30
camel a40c8cf5a4
binder: Let apps call SecurityPolicy.checkAuthorization() by PeerUid (#12257)
This allows a server with access to PeerUid to check additional application-layer security policy *after* the call itself is authorized by the transport layer. Cross cutting application-layer checks could be done from a ServerInterceptor (RPC method level policy, say). Checks based on the substance of a request message could be done by the individual RPC method implementations themselves.
2025-08-05 16:47:45 -07:00
Kannan J 8b46ad58c3
Start 1.76.0 development cycle (#12258) 2025-08-05 22:00:39 +05:30
81 changed files with 2289 additions and 764 deletions

View File

@ -2,13 +2,13 @@ module(
name = "grpc-java", name = "grpc-java",
compatibility_level = 0, compatibility_level = 0,
repo_name = "io_grpc_grpc_java", repo_name = "io_grpc_grpc_java",
version = "1.75.0-SNAPSHOT", # CURRENT_GRPC_VERSION version = "1.76.0-SNAPSHOT", # CURRENT_GRPC_VERSION
) )
# GRPC_DEPS_START # GRPC_DEPS_START
IO_GRPC_GRPC_JAVA_ARTIFACTS = [ IO_GRPC_GRPC_JAVA_ARTIFACTS = [
"com.google.android:annotations:4.1.1.4", "com.google.android:annotations:4.1.1.4",
"com.google.api.grpc:proto-google-common-protos:2.51.0", "com.google.api.grpc:proto-google-common-protos:2.59.2",
"com.google.auth:google-auth-library-credentials:1.24.1", "com.google.auth:google-auth-library-credentials:1.24.1",
"com.google.auth:google-auth-library-oauth2-http:1.24.1", "com.google.auth:google-auth-library-oauth2-http:1.24.1",
"com.google.auto.value:auto-value-annotations:1.11.0", "com.google.auto.value:auto-value-annotations:1.11.0",
@ -19,24 +19,24 @@ IO_GRPC_GRPC_JAVA_ARTIFACTS = [
"com.google.guava:failureaccess:1.0.1", "com.google.guava:failureaccess:1.0.1",
"com.google.guava:guava:33.3.1-android", "com.google.guava:guava:33.3.1-android",
"com.google.re2j:re2j:1.8", "com.google.re2j:re2j:1.8",
"com.google.s2a.proto.v2:s2a-proto:0.1.1", "com.google.s2a.proto.v2:s2a-proto:0.1.2",
"com.google.truth:truth:1.4.2", "com.google.truth:truth:1.4.2",
"com.squareup.okhttp:okhttp:2.7.5", "com.squareup.okhttp:okhttp:2.7.5",
"com.squareup.okio:okio:2.10.0", # 3.0+ needs swapping to -jvm; need work to avoid flag-day "com.squareup.okio:okio:2.10.0", # 3.0+ needs swapping to -jvm; need work to avoid flag-day
"io.netty:netty-buffer:4.1.110.Final", "io.netty:netty-buffer:4.1.124.Final",
"io.netty:netty-codec-http2:4.1.110.Final", "io.netty:netty-codec-http2:4.1.124.Final",
"io.netty:netty-codec-http:4.1.110.Final", "io.netty:netty-codec-http:4.1.124.Final",
"io.netty:netty-codec-socks:4.1.110.Final", "io.netty:netty-codec-socks:4.1.124.Final",
"io.netty:netty-codec:4.1.110.Final", "io.netty:netty-codec:4.1.124.Final",
"io.netty:netty-common:4.1.110.Final", "io.netty:netty-common:4.1.124.Final",
"io.netty:netty-handler-proxy:4.1.110.Final", "io.netty:netty-handler-proxy:4.1.124.Final",
"io.netty:netty-handler:4.1.110.Final", "io.netty:netty-handler:4.1.124.Final",
"io.netty:netty-resolver:4.1.110.Final", "io.netty:netty-resolver:4.1.124.Final",
"io.netty:netty-tcnative-boringssl-static:2.0.70.Final", "io.netty:netty-tcnative-boringssl-static:2.0.70.Final",
"io.netty:netty-tcnative-classes:2.0.70.Final", "io.netty:netty-tcnative-classes:2.0.70.Final",
"io.netty:netty-transport-native-epoll:jar:linux-x86_64:4.1.110.Final", "io.netty:netty-transport-native-epoll:jar:linux-x86_64:4.1.124.Final",
"io.netty:netty-transport-native-unix-common:4.1.110.Final", "io.netty:netty-transport-native-unix-common:4.1.124.Final",
"io.netty:netty-transport:4.1.110.Final", "io.netty:netty-transport:4.1.124.Final",
"io.opencensus:opencensus-api:0.31.0", "io.opencensus:opencensus-api:0.31.0",
"io.opencensus:opencensus-contrib-grpc-metrics:0.31.0", "io.opencensus:opencensus-contrib-grpc-metrics:0.31.0",
"io.perfmark:perfmark-api:0.27.0", "io.perfmark:perfmark-api:0.27.0",
@ -50,7 +50,8 @@ bazel_dep(name = "bazel_jar_jar", version = "0.1.7")
bazel_dep(name = "bazel_skylib", version = "1.7.1") bazel_dep(name = "bazel_skylib", version = "1.7.1")
bazel_dep(name = "googleapis", repo_name = "com_google_googleapis", version = "0.0.0-20240326-1c8d509c5") bazel_dep(name = "googleapis", repo_name = "com_google_googleapis", version = "0.0.0-20240326-1c8d509c5")
bazel_dep(name = "grpc-proto", repo_name = "io_grpc_grpc_proto", version = "0.0.0-20240627-ec30f58") bazel_dep(name = "grpc-proto", repo_name = "io_grpc_grpc_proto", version = "0.0.0-20240627-ec30f58")
bazel_dep(name = "protobuf", repo_name = "com_google_protobuf", version = "23.1") # Protobuf 25.5+ is incompatible with Bazel 7 with bzlmod
bazel_dep(name = "protobuf", repo_name = "com_google_protobuf", version = "24.4")
bazel_dep(name = "rules_cc", version = "0.0.9") bazel_dep(name = "rules_cc", version = "0.0.9")
bazel_dep(name = "rules_java", version = "5.3.5") bazel_dep(name = "rules_java", version = "5.3.5")
bazel_dep(name = "rules_jvm_external", version = "6.0") bazel_dep(name = "rules_jvm_external", version = "6.0")

View File

@ -44,8 +44,8 @@ For a guided tour, take a look at the [quick start
guide](https://grpc.io/docs/languages/java/quickstart) or the more explanatory [gRPC guide](https://grpc.io/docs/languages/java/quickstart) or the more explanatory [gRPC
basics](https://grpc.io/docs/languages/java/basics). basics](https://grpc.io/docs/languages/java/basics).
The [examples](https://github.com/grpc/grpc-java/tree/v1.74.0/examples) and the The [examples](https://github.com/grpc/grpc-java/tree/v1.75.0/examples) and the
[Android example](https://github.com/grpc/grpc-java/tree/v1.74.0/examples/android) [Android example](https://github.com/grpc/grpc-java/tree/v1.75.0/examples/android)
are standalone projects that showcase the usage of gRPC. are standalone projects that showcase the usage of gRPC.
Download Download
@ -56,18 +56,18 @@ Download [the JARs][]. Or for Maven with non-Android, add to your `pom.xml`:
<dependency> <dependency>
<groupId>io.grpc</groupId> <groupId>io.grpc</groupId>
<artifactId>grpc-netty-shaded</artifactId> <artifactId>grpc-netty-shaded</artifactId>
<version>1.74.0</version> <version>1.75.0</version>
<scope>runtime</scope> <scope>runtime</scope>
</dependency> </dependency>
<dependency> <dependency>
<groupId>io.grpc</groupId> <groupId>io.grpc</groupId>
<artifactId>grpc-protobuf</artifactId> <artifactId>grpc-protobuf</artifactId>
<version>1.74.0</version> <version>1.75.0</version>
</dependency> </dependency>
<dependency> <dependency>
<groupId>io.grpc</groupId> <groupId>io.grpc</groupId>
<artifactId>grpc-stub</artifactId> <artifactId>grpc-stub</artifactId>
<version>1.74.0</version> <version>1.75.0</version>
</dependency> </dependency>
<dependency> <!-- necessary for Java 9+ --> <dependency> <!-- necessary for Java 9+ -->
<groupId>org.apache.tomcat</groupId> <groupId>org.apache.tomcat</groupId>
@ -79,18 +79,18 @@ Download [the JARs][]. Or for Maven with non-Android, add to your `pom.xml`:
Or for Gradle with non-Android, add to your dependencies: Or for Gradle with non-Android, add to your dependencies:
```gradle ```gradle
runtimeOnly 'io.grpc:grpc-netty-shaded:1.74.0' runtimeOnly 'io.grpc:grpc-netty-shaded:1.75.0'
implementation 'io.grpc:grpc-protobuf:1.74.0' implementation 'io.grpc:grpc-protobuf:1.75.0'
implementation 'io.grpc:grpc-stub:1.74.0' implementation 'io.grpc:grpc-stub:1.75.0'
compileOnly 'org.apache.tomcat:annotations-api:6.0.53' // necessary for Java 9+ compileOnly 'org.apache.tomcat:annotations-api:6.0.53' // necessary for Java 9+
``` ```
For Android client, use `grpc-okhttp` instead of `grpc-netty-shaded` and For Android client, use `grpc-okhttp` instead of `grpc-netty-shaded` and
`grpc-protobuf-lite` instead of `grpc-protobuf`: `grpc-protobuf-lite` instead of `grpc-protobuf`:
```gradle ```gradle
implementation 'io.grpc:grpc-okhttp:1.74.0' implementation 'io.grpc:grpc-okhttp:1.75.0'
implementation 'io.grpc:grpc-protobuf-lite:1.74.0' implementation 'io.grpc:grpc-protobuf-lite:1.75.0'
implementation 'io.grpc:grpc-stub:1.74.0' implementation 'io.grpc:grpc-stub:1.75.0'
compileOnly 'org.apache.tomcat:annotations-api:6.0.53' // necessary for Java 9+ compileOnly 'org.apache.tomcat:annotations-api:6.0.53' // necessary for Java 9+
``` ```
@ -99,7 +99,7 @@ For [Bazel](https://bazel.build), you can either
(with the GAVs from above), or use `@io_grpc_grpc_java//api` et al (see below). (with the GAVs from above), or use `@io_grpc_grpc_java//api` et al (see below).
[the JARs]: [the JARs]:
https://search.maven.org/search?q=g:io.grpc%20AND%20v:1.74.0 https://search.maven.org/search?q=g:io.grpc%20AND%20v:1.75.0
Development snapshots are available in [Sonatypes's snapshot Development snapshots are available in [Sonatypes's snapshot
repository](https://central.sonatype.com/repository/maven-snapshots/). repository](https://central.sonatype.com/repository/maven-snapshots/).
@ -131,7 +131,7 @@ For protobuf-based codegen integrated with the Maven build system, you can use
<configuration> <configuration>
<protocArtifact>com.google.protobuf:protoc:3.25.5:exe:${os.detected.classifier}</protocArtifact> <protocArtifact>com.google.protobuf:protoc:3.25.5:exe:${os.detected.classifier}</protocArtifact>
<pluginId>grpc-java</pluginId> <pluginId>grpc-java</pluginId>
<pluginArtifact>io.grpc:protoc-gen-grpc-java:1.74.0:exe:${os.detected.classifier}</pluginArtifact> <pluginArtifact>io.grpc:protoc-gen-grpc-java:1.75.0:exe:${os.detected.classifier}</pluginArtifact>
</configuration> </configuration>
<executions> <executions>
<execution> <execution>
@ -161,7 +161,7 @@ protobuf {
} }
plugins { plugins {
grpc { grpc {
artifact = 'io.grpc:protoc-gen-grpc-java:1.74.0' artifact = 'io.grpc:protoc-gen-grpc-java:1.75.0'
} }
} }
generateProtoTasks { generateProtoTasks {
@ -194,7 +194,7 @@ protobuf {
} }
plugins { plugins {
grpc { grpc {
artifact = 'io.grpc:protoc-gen-grpc-java:1.74.0' artifact = 'io.grpc:protoc-gen-grpc-java:1.75.0'
} }
} }
generateProtoTasks { generateProtoTasks {

View File

@ -400,7 +400,8 @@ grpc-netty version | netty-handler version | netty-tcnative-boringssl-static ver
1.59.x | 4.1.97.Final | 2.0.61.Final 1.59.x | 4.1.97.Final | 2.0.61.Final
1.60.x-1.66.x | 4.1.100.Final | 2.0.61.Final 1.60.x-1.66.x | 4.1.100.Final | 2.0.61.Final
1.67.x-1.70.x | 4.1.110.Final | 2.0.65.Final 1.67.x-1.70.x | 4.1.110.Final | 2.0.65.Final
1.71.x- | 4.1.110.Final | 2.0.70.Final 1.71.x-1.74.x | 4.1.110.Final | 2.0.70.Final
1.75.x- | 4.1.124.Final | 2.0.72.Final
_(grpc-netty-shaded avoids issues with keeping these versions in sync.)_ _(grpc-netty-shaded avoids issues with keeping these versions in sync.)_

View File

@ -14,7 +14,7 @@ android {
} }
compileSdkVersion 34 compileSdkVersion 34
defaultConfig { defaultConfig {
minSdkVersion 21 minSdkVersion 22
targetSdkVersion 33 targetSdkVersion 33
versionCode 1 versionCode 1
versionName "1.0" versionName "1.0"

View File

@ -217,7 +217,6 @@ public final class AndroidChannelBuilder extends ForwardingChannelBuilder<Androi
connectivityManager.registerDefaultNetworkCallback(defaultNetworkCallback); connectivityManager.registerDefaultNetworkCallback(defaultNetworkCallback);
unregisterRunnable = unregisterRunnable =
new Runnable() { new Runnable() {
@TargetApi(Build.VERSION_CODES.LOLLIPOP)
@Override @Override
public void run() { public void run() {
connectivityManager.unregisterNetworkCallback(defaultNetworkCallback); connectivityManager.unregisterNetworkCallback(defaultNetworkCallback);
@ -231,7 +230,6 @@ public final class AndroidChannelBuilder extends ForwardingChannelBuilder<Androi
context.registerReceiver(networkReceiver, networkIntentFilter); context.registerReceiver(networkReceiver, networkIntentFilter);
unregisterRunnable = unregisterRunnable =
new Runnable() { new Runnable() {
@TargetApi(Build.VERSION_CODES.LOLLIPOP)
@Override @Override
public void run() { public void run() {
context.unregisterReceiver(networkReceiver); context.unregisterReceiver(networkReceiver);

View File

@ -166,6 +166,11 @@ public final class NameResolverRegistry {
} catch (ClassNotFoundException e) { } catch (ClassNotFoundException e) {
logger.log(Level.FINE, "Unable to find DNS NameResolver", e); logger.log(Level.FINE, "Unable to find DNS NameResolver", e);
} }
try {
list.add(Class.forName("io.grpc.binder.internal.IntentNameResolverProvider"));
} catch (ClassNotFoundException e) {
logger.log(Level.FINE, "Unable to find IntentNameResolverProvider", e);
}
return Collections.unmodifiableList(list); return Collections.unmodifiableList(list);
} }

View File

@ -13,7 +13,7 @@ android {
targetCompatibility 1.8 targetCompatibility 1.8
} }
defaultConfig { defaultConfig {
minSdkVersion 21 minSdkVersion 22
targetSdkVersion 33 targetSdkVersion 33
versionCode 1 versionCode 1
versionName "1.0" versionName "1.0"

View File

@ -11,11 +11,13 @@
<service android:name="io.grpc.binder.HostServices$HostService1" android:exported="false"> <service android:name="io.grpc.binder.HostServices$HostService1" android:exported="false">
<intent-filter> <intent-filter>
<action android:name="action1"/> <action android:name="action1"/>
<data android:scheme="scheme" android:host="authority" android:path="/path"/>
</intent-filter> </intent-filter>
</service> </service>
<service android:name="io.grpc.binder.HostServices$HostService2" android:exported="false"> <service android:name="io.grpc.binder.HostServices$HostService2" android:exported="false">
<intent-filter> <intent-filter>
<action android:name="action2"/> <action android:name="action2"/>
<data android:scheme="scheme" android:host="authority" android:path="/path"/>
</intent-filter> </intent-filter>
</service> </service>
</application> </application>

View File

@ -23,6 +23,7 @@ import static java.util.concurrent.TimeUnit.SECONDS;
import android.content.Context; import android.content.Context;
import android.content.Intent; import android.content.Intent;
import android.net.Uri;
import android.os.Parcel; import android.os.Parcel;
import android.os.Parcelable; import android.os.Parcelable;
import androidx.test.core.app.ApplicationProvider; import androidx.test.core.app.ApplicationProvider;
@ -39,7 +40,6 @@ import io.grpc.ForwardingServerCall.SimpleForwardingServerCall;
import io.grpc.ManagedChannel; import io.grpc.ManagedChannel;
import io.grpc.Metadata; import io.grpc.Metadata;
import io.grpc.MethodDescriptor; import io.grpc.MethodDescriptor;
import io.grpc.NameResolverRegistry;
import io.grpc.ServerCall; import io.grpc.ServerCall;
import io.grpc.ServerCall.Listener; import io.grpc.ServerCall.Listener;
import io.grpc.ServerCallHandler; import io.grpc.ServerCallHandler;
@ -49,7 +49,6 @@ import io.grpc.ServerServiceDefinition;
import io.grpc.Status.Code; import io.grpc.Status.Code;
import io.grpc.StatusRuntimeException; import io.grpc.StatusRuntimeException;
import io.grpc.internal.GrpcUtil; import io.grpc.internal.GrpcUtil;
import io.grpc.internal.testing.FakeNameResolverProvider;
import io.grpc.stub.ClientCalls; import io.grpc.stub.ClientCalls;
import io.grpc.stub.MetadataUtils; import io.grpc.stub.MetadataUtils;
import io.grpc.stub.ServerCalls; import io.grpc.stub.ServerCalls;
@ -77,7 +76,6 @@ public final class BinderChannelSmokeTest {
private static final int SLIGHTLY_MORE_THAN_ONE_BLOCK = 16 * 1024 + 100; private static final int SLIGHTLY_MORE_THAN_ONE_BLOCK = 16 * 1024 + 100;
private static final String MSG = "Some text which will be repeated many many times"; private static final String MSG = "Some text which will be repeated many many times";
private static final String SERVER_TARGET_URI = "fake://server";
private static final Metadata.Key<PoisonParcelable> POISON_KEY = private static final Metadata.Key<PoisonParcelable> POISON_KEY =
ParcelableUtils.metadataKey("poison-bin", PoisonParcelable.CREATOR); ParcelableUtils.metadataKey("poison-bin", PoisonParcelable.CREATOR);
@ -99,7 +97,6 @@ public final class BinderChannelSmokeTest {
.setType(MethodDescriptor.MethodType.BIDI_STREAMING) .setType(MethodDescriptor.MethodType.BIDI_STREAMING)
.build(); .build();
FakeNameResolverProvider fakeNameResolverProvider;
ManagedChannel channel; ManagedChannel channel;
AtomicReference<Metadata> headersCapture = new AtomicReference<>(); AtomicReference<Metadata> headersCapture = new AtomicReference<>();
AtomicReference<PeerUid> clientUidCapture = new AtomicReference<>(); AtomicReference<PeerUid> clientUidCapture = new AtomicReference<>();
@ -138,8 +135,6 @@ public final class BinderChannelSmokeTest {
PeerUids.newPeerIdentifyingServerInterceptor()); PeerUids.newPeerIdentifyingServerInterceptor());
AndroidComponentAddress serverAddress = HostServices.allocateService(appContext); AndroidComponentAddress serverAddress = HostServices.allocateService(appContext);
fakeNameResolverProvider = new FakeNameResolverProvider(SERVER_TARGET_URI, serverAddress);
NameResolverRegistry.getDefaultRegistry().register(fakeNameResolverProvider);
HostServices.configureService( HostServices.configureService(
serverAddress, serverAddress,
HostServices.serviceParamsBuilder() HostServices.serviceParamsBuilder()
@ -166,7 +161,6 @@ public final class BinderChannelSmokeTest {
@After @After
public void tearDown() throws Exception { public void tearDown() throws Exception {
channel.shutdownNow(); channel.shutdownNow();
NameResolverRegistry.getDefaultRegistry().deregister(fakeNameResolverProvider);
HostServices.awaitServiceShutdown(); HostServices.awaitServiceShutdown();
} }
@ -235,7 +229,11 @@ public final class BinderChannelSmokeTest {
@Test @Test
public void testConnectViaTargetUri() throws Exception { public void testConnectViaTargetUri() throws Exception {
channel = BinderChannelBuilder.forTarget(SERVER_TARGET_URI, appContext).build(); // Compare with the <intent-filter> mapping in AndroidManifest.xml.
channel =
BinderChannelBuilder.forTarget(
"intent://authority/path#Intent;action=action1;scheme=scheme;end;", appContext)
.build();
assertThat(doCall("Hello").get()).isEqualTo("Hello"); assertThat(doCall("Hello").get()).isEqualTo("Hello");
} }
@ -245,7 +243,10 @@ public final class BinderChannelSmokeTest {
channel = channel =
BinderChannelBuilder.forAddress( BinderChannelBuilder.forAddress(
AndroidComponentAddress.forBindIntent( AndroidComponentAddress.forBindIntent(
new Intent().setAction("action1").setPackage(appContext.getPackageName())), new Intent()
.setAction("action1")
.setData(Uri.parse("scheme://authority/path"))
.setPackage(appContext.getPackageName())),
appContext) appContext)
.build(); .build();
assertThat(doCall("Hello").get()).isEqualTo("Hello"); assertThat(doCall("Hello").get()).isEqualTo("Hello");

View File

@ -100,7 +100,7 @@ public final class BinderClientTransportTest {
.build(); .build();
AndroidComponentAddress serverAddress; AndroidComponentAddress serverAddress;
BinderTransport.BinderClientTransport transport; BinderClientTransport transport;
BlockingSecurityPolicy blockingSecurityPolicy = new BlockingSecurityPolicy(); BlockingSecurityPolicy blockingSecurityPolicy = new BlockingSecurityPolicy();
private final ObjectPool<ScheduledExecutorService> executorServicePool = private final ObjectPool<ScheduledExecutorService> executorServicePool =
@ -178,7 +178,7 @@ public final class BinderClientTransportTest {
return this; return this;
} }
public BinderTransport.BinderClientTransport build() { public BinderClientTransport build() {
return factoryBuilder return factoryBuilder
.buildClientTransportFactory() .buildClientTransportFactory()
.newClientTransport(serverAddress, new ClientTransportOptions(), null); .newClientTransport(serverAddress, new ClientTransportOptions(), null);
@ -502,8 +502,7 @@ public final class BinderClientTransportTest {
} }
private static void startAndAwaitReady( private static void startAndAwaitReady(
BinderTransport.BinderClientTransport transport, TestTransportListener transportListener) BinderClientTransport transport, TestTransportListener transportListener) throws Exception {
throws Exception {
transport.start(transportListener).run(); transport.start(transportListener).run();
transportListener.awaitReady(); transportListener.awaitReady();
} }

View File

@ -106,8 +106,7 @@ public final class BinderTransportTest extends AbstractTransportTest {
options.setEagAttributes(eagAttrs()); options.setEagAttributes(eagAttrs());
options.setChannelLogger(transportLogger()); options.setChannelLogger(transportLogger());
return new BinderTransport.BinderClientTransport( return new BinderClientTransport(builder.buildClientTransportFactory(), addr, options);
builder.buildClientTransportFactory(), addr, options);
} }
@Test @Test

View File

@ -34,6 +34,18 @@ public final class ApiConstants {
*/ */
public static final String ACTION_BIND = "grpc.io.action.BIND"; public static final String ACTION_BIND = "grpc.io.action.BIND";
/**
* Gives a {@link NameResolver} access to its Channel's "source" {@link android.content.Context},
* the entry point to almost every other Android API.
*
* <p>This argument is set automatically by {@link BinderChannelBuilder}. Any value passed to
* {@link io.grpc.ManagedChannelBuilder#setNameResolverArg} will be ignored.
*
* <p>See {@link BinderChannelBuilder#forTarget(String, android.content.Context)} for more.
*/
public static final NameResolver.Args.Key<android.content.Context> SOURCE_ANDROID_CONTEXT =
NameResolver.Args.Key.create("source-android-context");
/** /**
* Specifies the Android user in which target URIs should be resolved. * Specifies the Android user in which target URIs should be resolved.
* *

View File

@ -67,4 +67,25 @@ public abstract class AsyncSecurityPolicy extends SecurityPolicy {
* authorized. * authorized.
*/ */
public abstract ListenableFuture<Status> checkAuthorizationAsync(int uid); public abstract ListenableFuture<Status> checkAuthorizationAsync(int uid);
/**
* Decides whether the given Android UID is authorized, without providing its raw integer value.
*
* <p>Calling this is equivalent to calling {@link SecurityPolicy#checkAuthorization(int)}, except
* the caller provides a {@link PeerUid} wrapper instead of the raw integer uid (known only to the
* transport). This allows a server to check additional application-layer security policy for
* itself *after* the call itself is authorized by the transport layer. Cross cutting application-
* layer checks could be done from a {@link io.grpc.ServerInterceptor}. Checks based on the
* substance of a request message could be done by the individual RPC method implementations
* themselves.
*
* <p>See #checkAuthorizationAsync(int) for details on the semantics. See {@link
* PeerUids#newPeerIdentifyingServerInterceptor()} for how to get a {@link PeerUid}.
*
* @param uid The Android UID to authenticate.
* @return A gRPC {@link Status} object, with OK indicating authorized.
*/
public final ListenableFuture<Status> checkAuthorizationAsync(PeerUid uid) {
return checkAuthorizationAsync(uid.getUid());
}
} }

View File

@ -321,6 +321,8 @@ public final class BinderChannelBuilder extends ForwardingChannelBuilder<BinderC
public ManagedChannel build() { public ManagedChannel build() {
transportFactoryBuilder.setOffloadExecutorPool( transportFactoryBuilder.setOffloadExecutorPool(
managedChannelImplBuilder.getOffloadExecutorPool()); managedChannelImplBuilder.getOffloadExecutorPool());
setNameResolverArg(
ApiConstants.SOURCE_ANDROID_CONTEXT, transportFactoryBuilder.getSourceContext());
return super.build(); return super.build();
} }
} }

View File

@ -184,7 +184,6 @@ public final class SecurityPolicies {
* Creates {@link SecurityPolicy} which checks if the app is a device owner app. See {@link * Creates {@link SecurityPolicy} which checks if the app is a device owner app. See {@link
* DevicePolicyManager}. * DevicePolicyManager}.
*/ */
@RequiresApi(18)
public static io.grpc.binder.SecurityPolicy isDeviceOwner(Context applicationContext) { public static io.grpc.binder.SecurityPolicy isDeviceOwner(Context applicationContext) {
DevicePolicyManager devicePolicyManager = DevicePolicyManager devicePolicyManager =
(DevicePolicyManager) applicationContext.getSystemService(Context.DEVICE_POLICY_SERVICE); (DevicePolicyManager) applicationContext.getSystemService(Context.DEVICE_POLICY_SERVICE);
@ -199,7 +198,6 @@ public final class SecurityPolicies {
* Creates {@link SecurityPolicy} which checks if the app is a profile owner app. See {@link * Creates {@link SecurityPolicy} which checks if the app is a profile owner app. See {@link
* DevicePolicyManager}. * DevicePolicyManager}.
*/ */
@RequiresApi(21)
public static SecurityPolicy isProfileOwner(Context applicationContext) { public static SecurityPolicy isProfileOwner(Context applicationContext) {
DevicePolicyManager devicePolicyManager = DevicePolicyManager devicePolicyManager =
(DevicePolicyManager) applicationContext.getSystemService(Context.DEVICE_POLICY_SERVICE); (DevicePolicyManager) applicationContext.getSystemService(Context.DEVICE_POLICY_SERVICE);

View File

@ -53,4 +53,25 @@ public abstract class SecurityPolicy {
* @return A gRPC {@link Status} object, with OK indicating authorized. * @return A gRPC {@link Status} object, with OK indicating authorized.
*/ */
public abstract Status checkAuthorization(int uid); public abstract Status checkAuthorization(int uid);
/**
* Decides whether the given Android UID is authorized, without providing its raw integer value.
*
* <p>Calling this is equivalent to calling {@link SecurityPolicy#checkAuthorization(int)}, except
* the caller provides a {@link PeerUid} wrapper instead of the raw integer uid (known only to the
* transport). This allows a server to check additional application-layer security policy for
* itself *after* the call itself is authorized by the transport layer. Cross cutting application-
* layer checks could be done from a {@link io.grpc.ServerInterceptor}. Checks based on the
* substance of a request message could be done by the individual RPC method implementations
* themselves.
*
* <p>See #checkAuthorizationAsync(int) for details on the semantics. See {@link
* PeerUids#newPeerIdentifyingServerInterceptor()} for how to get a {@link PeerUid}.
*
* @param uid The Android UID to authenticate.
* @return A gRPC {@link Status} object, with OK indicating authorized.
*/
public final Status checkAuthorization(PeerUid uid) {
return checkAuthorization(uid.getUid());
}
} }

View File

@ -11,8 +11,8 @@ import io.grpc.internal.ServerTransport;
import io.grpc.internal.ServerTransportListener; import io.grpc.internal.ServerTransportListener;
/** /**
* Tracks which {@link BinderTransport.BinderServerTransport} are currently active and allows * Tracks which {@link BinderServerTransport} are currently active and allows invoking a {@link
* invoking a {@link Runnable} only once all transports are terminated. * Runnable} only once all transports are terminated.
*/ */
final class ActiveTransportTracker implements ServerListener { final class ActiveTransportTracker implements ServerListener {
private final ServerListener delegate; private final ServerListener delegate;

View File

@ -0,0 +1,441 @@
/*
* Copyright 2020 The 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.
*/
package io.grpc.binder.internal;
import static com.google.common.base.Preconditions.checkNotNull;
import static io.grpc.binder.ApiConstants.PRE_AUTH_SERVER_OVERRIDE;
import static java.util.concurrent.TimeUnit.MILLISECONDS;
import android.content.Context;
import android.content.pm.ServiceInfo;
import android.os.Binder;
import android.os.IBinder;
import android.os.Parcel;
import android.os.Process;
import com.google.common.base.Ticker;
import com.google.common.util.concurrent.FutureCallback;
import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture;
import com.google.errorprone.annotations.CheckReturnValue;
import com.google.errorprone.annotations.concurrent.GuardedBy;
import io.grpc.Attributes;
import io.grpc.CallOptions;
import io.grpc.ClientStreamTracer;
import io.grpc.Grpc;
import io.grpc.Internal;
import io.grpc.InternalLogId;
import io.grpc.Metadata;
import io.grpc.MethodDescriptor;
import io.grpc.SecurityLevel;
import io.grpc.Status;
import io.grpc.StatusException;
import io.grpc.binder.AndroidComponentAddress;
import io.grpc.binder.AsyncSecurityPolicy;
import io.grpc.binder.InboundParcelablePolicy;
import io.grpc.binder.SecurityPolicy;
import io.grpc.internal.ClientStream;
import io.grpc.internal.ClientTransportFactory.ClientTransportOptions;
import io.grpc.internal.ConnectionClientTransport;
import io.grpc.internal.FailingClientStream;
import io.grpc.internal.GrpcAttributes;
import io.grpc.internal.GrpcUtil;
import io.grpc.internal.ManagedClientTransport;
import io.grpc.internal.ObjectPool;
import io.grpc.internal.StatsTraceContext;
import java.util.concurrent.Executor;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.atomic.AtomicInteger;
import javax.annotation.Nullable;
import javax.annotation.concurrent.ThreadSafe;
/** Concrete client-side transport implementation. */
@ThreadSafe
@Internal
public final class BinderClientTransport extends BinderTransport
implements ConnectionClientTransport, Bindable.Observer {
private final ObjectPool<? extends Executor> offloadExecutorPool;
private final Executor offloadExecutor;
private final SecurityPolicy securityPolicy;
private final Bindable serviceBinding;
/** Number of ongoing calls which keep this transport "in-use". */
private final AtomicInteger numInUseStreams;
private final long readyTimeoutMillis;
private final PingTracker pingTracker;
private final boolean preAuthorizeServer;
@Nullable private ManagedClientTransport.Listener clientTransportListener;
@GuardedBy("this")
private int latestCallId = FIRST_CALL_ID;
@GuardedBy("this")
private ScheduledFuture<?> readyTimeoutFuture; // != null iff timeout scheduled.
@GuardedBy("this")
@Nullable
private ListenableFuture<Status> authResultFuture; // null before we check auth.
@GuardedBy("this")
@Nullable
private ListenableFuture<Status> preAuthResultFuture; // null before we pre-auth.
/**
* Constructs a new transport instance.
*
* @param factory parameters common to all a Channel's transports
* @param targetAddress the fully resolved and load-balanced server address
* @param options other parameters that can vary as transports come and go within a Channel
*/
public BinderClientTransport(
BinderClientTransportFactory factory,
AndroidComponentAddress targetAddress,
ClientTransportOptions options) {
super(
factory.scheduledExecutorPool,
buildClientAttributes(
options.getEagAttributes(),
factory.sourceContext,
targetAddress,
factory.inboundParcelablePolicy),
factory.binderDecorator,
buildLogId(factory.sourceContext, targetAddress));
this.offloadExecutorPool = factory.offloadExecutorPool;
this.securityPolicy = factory.securityPolicy;
this.offloadExecutor = offloadExecutorPool.getObject();
this.readyTimeoutMillis = factory.readyTimeoutMillis;
Boolean preAuthServerOverride = options.getEagAttributes().get(PRE_AUTH_SERVER_OVERRIDE);
this.preAuthorizeServer =
preAuthServerOverride != null ? preAuthServerOverride : factory.preAuthorizeServers;
numInUseStreams = new AtomicInteger();
pingTracker = new PingTracker(Ticker.systemTicker(), (id) -> sendPing(id));
serviceBinding =
new ServiceBinding(
factory.mainThreadExecutor,
factory.sourceContext,
factory.channelCredentials,
targetAddress.asBindIntent(),
targetAddress.getTargetUser() != null
? targetAddress.getTargetUser()
: factory.defaultTargetUserHandle,
factory.bindServiceFlags.toInteger(),
this);
}
@Override
void releaseExecutors() {
super.releaseExecutors();
offloadExecutorPool.returnObject(offloadExecutor);
}
@Override
public synchronized void onBound(IBinder binder) {
sendSetupTransaction(binderDecorator.decorate(OneWayBinderProxy.wrap(binder, offloadExecutor)));
}
@Override
public synchronized void onUnbound(Status reason) {
shutdownInternal(reason, true);
}
@CheckReturnValue
@Override
public synchronized Runnable start(Listener clientTransportListener) {
this.clientTransportListener = checkNotNull(clientTransportListener);
return () -> {
synchronized (BinderClientTransport.this) {
if (inState(TransportState.NOT_STARTED)) {
setState(TransportState.SETUP);
try {
if (preAuthorizeServer) {
preAuthorize(serviceBinding.resolve());
} else {
serviceBinding.bind();
}
} catch (StatusException e) {
shutdownInternal(e.getStatus(), true);
return;
}
if (readyTimeoutMillis >= 0) {
readyTimeoutFuture =
getScheduledExecutorService()
.schedule(
BinderClientTransport.this::onReadyTimeout,
readyTimeoutMillis,
MILLISECONDS);
}
}
}
};
}
@GuardedBy("this")
private void preAuthorize(ServiceInfo serviceInfo) {
// It's unlikely, but the identity/existence of this Service could change by the time we
// actually connect. It doesn't matter though, because:
// - If pre-auth fails (but would succeed against the server's new state), the grpc-core layer
// will eventually retry using a new transport instance that will see the Service's new state.
// - If pre-auth succeeds (but would fail against the server's new state), we might give an
// unauthorized server a chance to run, but the connection will still fail by SecurityPolicy
// check later in handshake. Pre-auth remains effective at mitigating abuse because malware
// can't typically control the exact timing of its installation.
preAuthResultFuture = checkServerAuthorizationAsync(serviceInfo.applicationInfo.uid);
Futures.addCallback(
preAuthResultFuture,
new FutureCallback<Status>() {
@Override
public void onSuccess(Status result) {
handlePreAuthResult(result);
}
@Override
public void onFailure(Throwable t) {
handleAuthResult(t);
}
},
offloadExecutor);
}
private synchronized void handlePreAuthResult(Status authorization) {
if (inState(TransportState.SETUP)) {
if (!authorization.isOk()) {
shutdownInternal(authorization, true);
} else {
serviceBinding.bind();
}
}
}
private synchronized void onReadyTimeout() {
if (inState(TransportState.SETUP)) {
readyTimeoutFuture = null;
shutdownInternal(
Status.DEADLINE_EXCEEDED.withDescription(
"Connect timeout " + readyTimeoutMillis + "ms lapsed"),
true);
}
}
@Override
public synchronized ClientStream newStream(
final MethodDescriptor<?, ?> method,
final Metadata headers,
final CallOptions callOptions,
ClientStreamTracer[] tracers) {
if (!inState(TransportState.READY)) {
return newFailingClientStream(
isShutdown()
? shutdownStatus
: Status.INTERNAL.withDescription("newStream() before transportReady()"),
attributes,
headers,
tracers);
}
int callId = latestCallId++;
if (latestCallId == LAST_CALL_ID) {
latestCallId = FIRST_CALL_ID;
}
StatsTraceContext statsTraceContext =
StatsTraceContext.newClientContext(tracers, attributes, headers);
Inbound.ClientInbound inbound =
new Inbound.ClientInbound(
this, attributes, callId, GrpcUtil.shouldBeCountedForInUse(callOptions));
if (ongoingCalls.putIfAbsent(callId, inbound) != null) {
Status failure = Status.INTERNAL.withDescription("Clashing call IDs");
shutdownInternal(failure, true);
return newFailingClientStream(failure, attributes, headers, tracers);
} else {
if (inbound.countsForInUse() && numInUseStreams.getAndIncrement() == 0) {
clientTransportListener.transportInUse(true);
}
Outbound.ClientOutbound outbound =
new Outbound.ClientOutbound(this, callId, method, headers, statsTraceContext);
if (method.getType().clientSendsOneMessage()) {
return new SingleMessageClientStream(inbound, outbound, attributes);
} else {
return new MultiMessageClientStream(inbound, outbound, attributes);
}
}
}
@Override
protected void unregisterInbound(Inbound<?> inbound) {
if (inbound.countsForInUse() && numInUseStreams.decrementAndGet() == 0) {
clientTransportListener.transportInUse(false);
}
super.unregisterInbound(inbound);
}
@Override
public void ping(final PingCallback callback, Executor executor) {
pingTracker.startPing(callback, executor);
}
@Override
public synchronized void shutdown(Status reason) {
checkNotNull(reason, "reason");
shutdownInternal(reason, false);
}
@Override
public synchronized void shutdownNow(Status reason) {
checkNotNull(reason, "reason");
shutdownInternal(reason, true);
}
@Override
@GuardedBy("this")
void notifyShutdown(Status status) {
clientTransportListener.transportShutdown(status);
}
@Override
@GuardedBy("this")
void notifyTerminated() {
if (numInUseStreams.getAndSet(0) > 0) {
clientTransportListener.transportInUse(false);
}
if (readyTimeoutFuture != null) {
readyTimeoutFuture.cancel(false);
readyTimeoutFuture = null;
}
if (preAuthResultFuture != null) {
preAuthResultFuture.cancel(false); // No effect if already complete.
}
if (authResultFuture != null) {
authResultFuture.cancel(false); // No effect if already complete.
}
serviceBinding.unbind();
clientTransportListener.transportTerminated();
}
@Override
@GuardedBy("this")
protected void handleSetupTransport(Parcel parcel) {
int remoteUid = Binder.getCallingUid();
attributes = setSecurityAttrs(attributes, remoteUid);
if (inState(TransportState.SETUP)) {
int version = parcel.readInt();
IBinder binder = parcel.readStrongBinder();
if (version != WIRE_FORMAT_VERSION) {
shutdownInternal(Status.UNAVAILABLE.withDescription("Wire format version mismatch"), true);
} else if (binder == null) {
shutdownInternal(
Status.UNAVAILABLE.withDescription("Malformed SETUP_TRANSPORT data"), true);
} else {
authResultFuture = checkServerAuthorizationAsync(remoteUid);
Futures.addCallback(
authResultFuture,
new FutureCallback<Status>() {
@Override
public void onSuccess(Status result) {
handleAuthResult(binder, result);
}
@Override
public void onFailure(Throwable t) {
handleAuthResult(t);
}
},
offloadExecutor);
}
}
}
private ListenableFuture<Status> checkServerAuthorizationAsync(int remoteUid) {
return (securityPolicy instanceof AsyncSecurityPolicy)
? ((AsyncSecurityPolicy) securityPolicy).checkAuthorizationAsync(remoteUid)
: Futures.submit(() -> securityPolicy.checkAuthorization(remoteUid), offloadExecutor);
}
private synchronized void handleAuthResult(IBinder binder, Status authorization) {
if (inState(TransportState.SETUP)) {
if (!authorization.isOk()) {
shutdownInternal(authorization, true);
} else if (!setOutgoingBinder(OneWayBinderProxy.wrap(binder, offloadExecutor))) {
shutdownInternal(
Status.UNAVAILABLE.withDescription("Failed to observe outgoing binder"), true);
} else {
// Check state again, since a failure inside setOutgoingBinder (or a callback it
// triggers), could have shut us down.
if (!isShutdown()) {
setState(TransportState.READY);
attributes = clientTransportListener.filterTransport(attributes);
clientTransportListener.transportReady();
if (readyTimeoutFuture != null) {
readyTimeoutFuture.cancel(false);
readyTimeoutFuture = null;
}
}
}
}
}
private synchronized void handleAuthResult(Throwable t) {
shutdownInternal(
Status.INTERNAL.withDescription("Could not evaluate SecurityPolicy").withCause(t), true);
}
@GuardedBy("this")
@Override
protected void handlePingResponse(Parcel parcel) {
pingTracker.onPingResponse(parcel.readInt());
}
private static ClientStream newFailingClientStream(
Status failure, Attributes attributes, Metadata headers, ClientStreamTracer[] tracers) {
StatsTraceContext statsTraceContext =
StatsTraceContext.newClientContext(tracers, attributes, headers);
statsTraceContext.clientOutboundHeaders();
return new FailingClientStream(failure, tracers);
}
private static InternalLogId buildLogId(
Context sourceContext, AndroidComponentAddress targetAddress) {
return InternalLogId.allocate(
BinderClientTransport.class,
sourceContext.getClass().getSimpleName() + "->" + targetAddress);
}
private static Attributes buildClientAttributes(
Attributes eagAttrs,
Context sourceContext,
AndroidComponentAddress targetAddress,
InboundParcelablePolicy inboundParcelablePolicy) {
return Attributes.newBuilder()
.set(GrpcAttributes.ATTR_SECURITY_LEVEL, SecurityLevel.NONE) // Trust noone for now.
.set(GrpcAttributes.ATTR_CLIENT_EAG_ATTRS, eagAttrs)
.set(Grpc.TRANSPORT_ATTR_LOCAL_ADDR, AndroidComponentAddress.forContext(sourceContext))
.set(Grpc.TRANSPORT_ATTR_REMOTE_ADDR, targetAddress)
.set(INBOUND_PARCELABLE_POLICY, inboundParcelablePolicy)
.build();
}
private static Attributes setSecurityAttrs(Attributes attributes, int uid) {
return attributes.toBuilder()
.set(REMOTE_UID, uid)
.set(
GrpcAttributes.ATTR_SECURITY_LEVEL,
uid == Process.myUid()
? SecurityLevel.PRIVACY_AND_INTEGRITY
: SecurityLevel.INTEGRITY) // TODO: Have the SecrityPolicy decide this.
.build();
}
}

View File

@ -83,12 +83,12 @@ public final class BinderClientTransportFactory implements ClientTransportFactor
} }
@Override @Override
public BinderTransport.BinderClientTransport newClientTransport( public BinderClientTransport newClientTransport(
SocketAddress addr, ClientTransportOptions options, ChannelLogger channelLogger) { SocketAddress addr, ClientTransportOptions options, ChannelLogger channelLogger) {
if (closed) { if (closed) {
throw new IllegalStateException("The transport factory is closed."); throw new IllegalStateException("The transport factory is closed.");
} }
return new BinderTransport.BinderClientTransport(this, (AndroidComponentAddress) addr, options); return new BinderClientTransport(this, (AndroidComponentAddress) addr, options);
} }
@Override @Override
@ -142,6 +142,10 @@ public final class BinderClientTransportFactory implements ClientTransportFactor
return this; return this;
} }
public Context getSourceContext() {
return sourceContext;
}
public Builder setOffloadExecutorPool(ObjectPool<? extends Executor> offloadExecutorPool) { public Builder setOffloadExecutorPool(ObjectPool<? extends Executor> offloadExecutorPool) {
this.offloadExecutorPool = checkNotNull(offloadExecutorPool, "offloadExecutorPool"); this.offloadExecutorPool = checkNotNull(offloadExecutorPool, "offloadExecutorPool");
return this; return this;

View File

@ -178,8 +178,8 @@ public final class BinderServer implements InternalServer, LeakSafeOneWayBinder.
serverPolicyChecker, serverPolicyChecker,
checkNotNull(executor, "Not started?")); checkNotNull(executor, "Not started?"));
// Create a new transport and let our listener know about it. // Create a new transport and let our listener know about it.
BinderTransport.BinderServerTransport transport = BinderServerTransport transport =
new BinderTransport.BinderServerTransport( new BinderServerTransport(
executorServicePool, executorServicePool,
attrsBuilder.build(), attrsBuilder.build(),
streamTracerFactories, streamTracerFactories,

View File

@ -0,0 +1,126 @@
/*
* Copyright 2020 The 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.
*/
package io.grpc.binder.internal;
import android.os.IBinder;
import com.google.errorprone.annotations.concurrent.GuardedBy;
import io.grpc.Attributes;
import io.grpc.Grpc;
import io.grpc.Internal;
import io.grpc.InternalLogId;
import io.grpc.Metadata;
import io.grpc.ServerStreamTracer;
import io.grpc.Status;
import io.grpc.internal.ObjectPool;
import io.grpc.internal.ServerStream;
import io.grpc.internal.ServerTransport;
import io.grpc.internal.ServerTransportListener;
import io.grpc.internal.StatsTraceContext;
import java.util.List;
import java.util.concurrent.ScheduledExecutorService;
import javax.annotation.Nullable;
/** Concrete server-side transport implementation. */
@Internal
public final class BinderServerTransport extends BinderTransport implements ServerTransport {
private final List<ServerStreamTracer.Factory> streamTracerFactories;
@Nullable private ServerTransportListener serverTransportListener;
/**
* Constructs a new transport instance.
*
* @param binderDecorator used to decorate 'callbackBinder', for fault injection.
*/
public BinderServerTransport(
ObjectPool<ScheduledExecutorService> executorServicePool,
Attributes attributes,
List<ServerStreamTracer.Factory> streamTracerFactories,
OneWayBinderProxy.Decorator binderDecorator,
IBinder callbackBinder) {
super(executorServicePool, attributes, binderDecorator, buildLogId(attributes));
this.streamTracerFactories = streamTracerFactories;
// TODO(jdcormie): Plumb in the Server's executor() and use it here instead.
setOutgoingBinder(OneWayBinderProxy.wrap(callbackBinder, getScheduledExecutorService()));
}
public synchronized void setServerTransportListener(
ServerTransportListener serverTransportListener) {
this.serverTransportListener = serverTransportListener;
if (isShutdown()) {
setState(TransportState.SHUTDOWN_TERMINATED);
notifyTerminated();
releaseExecutors();
} else {
sendSetupTransaction();
// Check we're not shutdown again, since a failure inside sendSetupTransaction (or a callback
// it triggers), could have shut us down.
if (!isShutdown()) {
setState(TransportState.READY);
attributes = serverTransportListener.transportReady(attributes);
}
}
}
StatsTraceContext createStatsTraceContext(String methodName, Metadata headers) {
return StatsTraceContext.newServerContext(streamTracerFactories, methodName, headers);
}
synchronized Status startStream(ServerStream stream, String methodName, Metadata headers) {
if (isShutdown()) {
return Status.UNAVAILABLE.withDescription("transport is shutdown");
} else {
serverTransportListener.streamCreated(stream, methodName, headers);
return Status.OK;
}
}
@Override
@GuardedBy("this")
void notifyShutdown(Status status) {
// Nothing to do.
}
@Override
@GuardedBy("this")
void notifyTerminated() {
if (serverTransportListener != null) {
serverTransportListener.transportTerminated();
}
}
@Override
public synchronized void shutdown() {
shutdownInternal(Status.OK, false);
}
@Override
public synchronized void shutdownNow(Status reason) {
shutdownInternal(reason, true);
}
@Override
@Nullable
@GuardedBy("this")
protected Inbound<?> createInbound(int callId) {
return new Inbound.ServerInbound(this, attributes, callId);
}
private static InternalLogId buildLogId(Attributes attributes) {
return InternalLogId.allocate(
BinderServerTransport.class, "from " + attributes.get(Grpc.TRANSPORT_ATTR_REMOTE_ADDR));
}
}

View File

@ -19,67 +19,32 @@ package io.grpc.binder.internal;
import static com.google.common.base.Preconditions.checkNotNull; import static com.google.common.base.Preconditions.checkNotNull;
import static com.google.common.base.Preconditions.checkState; import static com.google.common.base.Preconditions.checkState;
import static com.google.common.util.concurrent.Futures.immediateFuture; import static com.google.common.util.concurrent.Futures.immediateFuture;
import static io.grpc.binder.ApiConstants.PRE_AUTH_SERVER_OVERRIDE;
import static java.util.concurrent.TimeUnit.MILLISECONDS;
import android.content.Context;
import android.content.pm.ServiceInfo;
import android.os.Binder;
import android.os.DeadObjectException; import android.os.DeadObjectException;
import android.os.IBinder; import android.os.IBinder;
import android.os.Parcel; import android.os.Parcel;
import android.os.Process;
import android.os.RemoteException; import android.os.RemoteException;
import android.os.TransactionTooLargeException; import android.os.TransactionTooLargeException;
import androidx.annotation.BinderThread; import androidx.annotation.BinderThread;
import com.google.common.annotations.VisibleForTesting; import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Ticker;
import com.google.common.base.Verify; import com.google.common.base.Verify;
import com.google.common.util.concurrent.FutureCallback;
import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture; import com.google.common.util.concurrent.ListenableFuture;
import com.google.errorprone.annotations.CheckReturnValue;
import com.google.errorprone.annotations.concurrent.GuardedBy; import com.google.errorprone.annotations.concurrent.GuardedBy;
import io.grpc.Attributes; import io.grpc.Attributes;
import io.grpc.CallOptions;
import io.grpc.ClientStreamTracer;
import io.grpc.Grpc;
import io.grpc.Internal; import io.grpc.Internal;
import io.grpc.InternalChannelz.SocketStats; import io.grpc.InternalChannelz.SocketStats;
import io.grpc.InternalLogId; import io.grpc.InternalLogId;
import io.grpc.Metadata;
import io.grpc.MethodDescriptor;
import io.grpc.SecurityLevel;
import io.grpc.ServerStreamTracer;
import io.grpc.Status; import io.grpc.Status;
import io.grpc.StatusException; import io.grpc.StatusException;
import io.grpc.binder.AndroidComponentAddress;
import io.grpc.binder.AsyncSecurityPolicy;
import io.grpc.binder.InboundParcelablePolicy; import io.grpc.binder.InboundParcelablePolicy;
import io.grpc.binder.SecurityPolicy;
import io.grpc.internal.ClientStream;
import io.grpc.internal.ClientTransportFactory.ClientTransportOptions;
import io.grpc.internal.ConnectionClientTransport;
import io.grpc.internal.FailingClientStream;
import io.grpc.internal.GrpcAttributes;
import io.grpc.internal.GrpcUtil;
import io.grpc.internal.ManagedClientTransport;
import io.grpc.internal.ObjectPool; import io.grpc.internal.ObjectPool;
import io.grpc.internal.ServerStream;
import io.grpc.internal.ServerTransport;
import io.grpc.internal.ServerTransportListener;
import io.grpc.internal.StatsTraceContext;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Iterator; import java.util.Iterator;
import java.util.LinkedHashSet; import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.NoSuchElementException; import java.util.NoSuchElementException;
import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.Executor;
import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.logging.Level; import java.util.logging.Level;
import java.util.logging.Logger; import java.util.logging.Logger;
import javax.annotation.Nullable; import javax.annotation.Nullable;
@ -169,10 +134,10 @@ public abstract class BinderTransport implements IBinder.DeathRecipient {
private static final int RESERVED_TRANSACTIONS = 1000; private static final int RESERVED_TRANSACTIONS = 1000;
/** The first call ID we can use. */ /** The first call ID we can use. */
private static final int FIRST_CALL_ID = IBinder.FIRST_CALL_TRANSACTION + RESERVED_TRANSACTIONS; static final int FIRST_CALL_ID = IBinder.FIRST_CALL_TRANSACTION + RESERVED_TRANSACTIONS;
/** The last call ID we can use. */ /** The last call ID we can use. */
private static final int LAST_CALL_ID = IBinder.LAST_CALL_TRANSACTION; static final int LAST_CALL_ID = IBinder.LAST_CALL_TRANSACTION;
/** The states of this transport. */ /** The states of this transport. */
protected enum TransportState { protected enum TransportState {
@ -218,7 +183,7 @@ public abstract class BinderTransport implements IBinder.DeathRecipient {
// Only read/written on @BinderThread. // Only read/written on @BinderThread.
private long acknowledgedIncomingBytes; private long acknowledgedIncomingBytes;
private BinderTransport( protected BinderTransport(
ObjectPool<ScheduledExecutorService> executorServicePool, ObjectPool<ScheduledExecutorService> executorServicePool,
Attributes attributes, Attributes attributes,
OneWayBinderProxy.Decorator binderDecorator, OneWayBinderProxy.Decorator binderDecorator,
@ -559,478 +524,6 @@ public abstract class BinderTransport implements IBinder.DeathRecipient {
} }
} }
/** Concrete client-side transport implementation. */
@ThreadSafe
@Internal
public static final class BinderClientTransport extends BinderTransport
implements ConnectionClientTransport, Bindable.Observer {
private final ObjectPool<? extends Executor> offloadExecutorPool;
private final Executor offloadExecutor;
private final SecurityPolicy securityPolicy;
private final Bindable serviceBinding;
/** Number of ongoing calls which keep this transport "in-use". */
private final AtomicInteger numInUseStreams;
private final long readyTimeoutMillis;
private final PingTracker pingTracker;
private final boolean preAuthorizeServer;
@Nullable private ManagedClientTransport.Listener clientTransportListener;
@GuardedBy("this")
private int latestCallId = FIRST_CALL_ID;
@GuardedBy("this")
private ScheduledFuture<?> readyTimeoutFuture; // != null iff timeout scheduled.
@GuardedBy("this")
@Nullable private ListenableFuture<Status> authResultFuture; // null before we check auth.
@GuardedBy("this")
@Nullable
private ListenableFuture<Status> preAuthResultFuture; // null before we pre-auth.
/**
* Constructs a new transport instance.
*
* @param factory parameters common to all a Channel's transports
* @param targetAddress the fully resolved and load-balanced server address
* @param options other parameters that can vary as transports come and go within a Channel
*/
public BinderClientTransport(
BinderClientTransportFactory factory,
AndroidComponentAddress targetAddress,
ClientTransportOptions options) {
super(
factory.scheduledExecutorPool,
buildClientAttributes(
options.getEagAttributes(),
factory.sourceContext,
targetAddress,
factory.inboundParcelablePolicy),
factory.binderDecorator,
buildLogId(factory.sourceContext, targetAddress));
this.offloadExecutorPool = factory.offloadExecutorPool;
this.securityPolicy = factory.securityPolicy;
this.offloadExecutor = offloadExecutorPool.getObject();
this.readyTimeoutMillis = factory.readyTimeoutMillis;
Boolean preAuthServerOverride = options.getEagAttributes().get(PRE_AUTH_SERVER_OVERRIDE);
this.preAuthorizeServer =
preAuthServerOverride != null ? preAuthServerOverride : factory.preAuthorizeServers;
numInUseStreams = new AtomicInteger();
pingTracker = new PingTracker(Ticker.systemTicker(), (id) -> sendPing(id));
serviceBinding =
new ServiceBinding(
factory.mainThreadExecutor,
factory.sourceContext,
factory.channelCredentials,
targetAddress.asBindIntent(),
targetAddress.getTargetUser() != null
? targetAddress.getTargetUser()
: factory.defaultTargetUserHandle,
factory.bindServiceFlags.toInteger(),
this);
}
@Override
void releaseExecutors() {
super.releaseExecutors();
offloadExecutorPool.returnObject(offloadExecutor);
}
@Override
public synchronized void onBound(IBinder binder) {
sendSetupTransaction(
binderDecorator.decorate(OneWayBinderProxy.wrap(binder, offloadExecutor)));
}
@Override
public synchronized void onUnbound(Status reason) {
shutdownInternal(reason, true);
}
@CheckReturnValue
@Override
public synchronized Runnable start(ManagedClientTransport.Listener clientTransportListener) {
this.clientTransportListener = checkNotNull(clientTransportListener);
return () -> {
synchronized (BinderClientTransport.this) {
if (inState(TransportState.NOT_STARTED)) {
setState(TransportState.SETUP);
try {
if (preAuthorizeServer) {
preAuthorize(serviceBinding.resolve());
} else {
serviceBinding.bind();
}
} catch (StatusException e) {
shutdownInternal(e.getStatus(), true);
return;
}
if (readyTimeoutMillis >= 0) {
readyTimeoutFuture =
getScheduledExecutorService()
.schedule(
BinderClientTransport.this::onReadyTimeout,
readyTimeoutMillis,
MILLISECONDS);
}
}
}
};
}
@GuardedBy("this")
private void preAuthorize(ServiceInfo serviceInfo) {
// It's unlikely, but the identity/existence of this Service could change by the time we
// actually connect. It doesn't matter though, because:
// - If pre-auth fails (but would succeed against the server's new state), the grpc-core layer
// will eventually retry using a new transport instance that will see the Service's new state.
// - If pre-auth succeeds (but would fail against the server's new state), we might give an
// unauthorized server a chance to run, but the connection will still fail by SecurityPolicy
// check later in handshake. Pre-auth remains effective at mitigating abuse because malware
// can't typically control the exact timing of its installation.
preAuthResultFuture = checkServerAuthorizationAsync(serviceInfo.applicationInfo.uid);
Futures.addCallback(
preAuthResultFuture,
new FutureCallback<Status>() {
@Override
public void onSuccess(Status result) {
handlePreAuthResult(result);
}
@Override
public void onFailure(Throwable t) {
handleAuthResult(t);
}
},
offloadExecutor);
}
private synchronized void handlePreAuthResult(Status authorization) {
if (inState(TransportState.SETUP)) {
if (!authorization.isOk()) {
shutdownInternal(authorization, true);
} else {
serviceBinding.bind();
}
}
}
private synchronized void onReadyTimeout() {
if (inState(TransportState.SETUP)) {
readyTimeoutFuture = null;
shutdownInternal(
Status.DEADLINE_EXCEEDED.withDescription(
"Connect timeout " + readyTimeoutMillis + "ms lapsed"),
true);
}
}
@Override
public synchronized ClientStream newStream(
final MethodDescriptor<?, ?> method,
final Metadata headers,
final CallOptions callOptions,
ClientStreamTracer[] tracers) {
if (!inState(TransportState.READY)) {
return newFailingClientStream(
isShutdown()
? shutdownStatus
: Status.INTERNAL.withDescription("newStream() before transportReady()"),
attributes,
headers,
tracers);
}
int callId = latestCallId++;
if (latestCallId == LAST_CALL_ID) {
latestCallId = FIRST_CALL_ID;
}
StatsTraceContext statsTraceContext =
StatsTraceContext.newClientContext(tracers, attributes, headers);
Inbound.ClientInbound inbound =
new Inbound.ClientInbound(
this, attributes, callId, GrpcUtil.shouldBeCountedForInUse(callOptions));
if (ongoingCalls.putIfAbsent(callId, inbound) != null) {
Status failure = Status.INTERNAL.withDescription("Clashing call IDs");
shutdownInternal(failure, true);
return newFailingClientStream(failure, attributes, headers, tracers);
} else {
if (inbound.countsForInUse() && numInUseStreams.getAndIncrement() == 0) {
clientTransportListener.transportInUse(true);
}
Outbound.ClientOutbound outbound =
new Outbound.ClientOutbound(this, callId, method, headers, statsTraceContext);
if (method.getType().clientSendsOneMessage()) {
return new SingleMessageClientStream(inbound, outbound, attributes);
} else {
return new MultiMessageClientStream(inbound, outbound, attributes);
}
}
}
@Override
protected void unregisterInbound(Inbound<?> inbound) {
if (inbound.countsForInUse() && numInUseStreams.decrementAndGet() == 0) {
clientTransportListener.transportInUse(false);
}
super.unregisterInbound(inbound);
}
@Override
public void ping(final PingCallback callback, Executor executor) {
pingTracker.startPing(callback, executor);
}
@Override
public synchronized void shutdown(Status reason) {
checkNotNull(reason, "reason");
shutdownInternal(reason, false);
}
@Override
public synchronized void shutdownNow(Status reason) {
checkNotNull(reason, "reason");
shutdownInternal(reason, true);
}
@Override
@GuardedBy("this")
void notifyShutdown(Status status) {
clientTransportListener.transportShutdown(status);
}
@Override
@GuardedBy("this")
void notifyTerminated() {
if (numInUseStreams.getAndSet(0) > 0) {
clientTransportListener.transportInUse(false);
}
if (readyTimeoutFuture != null) {
readyTimeoutFuture.cancel(false);
readyTimeoutFuture = null;
}
if (preAuthResultFuture != null) {
preAuthResultFuture.cancel(false); // No effect if already complete.
}
if (authResultFuture != null) {
authResultFuture.cancel(false); // No effect if already complete.
}
serviceBinding.unbind();
clientTransportListener.transportTerminated();
}
@Override
@GuardedBy("this")
protected void handleSetupTransport(Parcel parcel) {
int remoteUid = Binder.getCallingUid();
attributes = setSecurityAttrs(attributes, remoteUid);
if (inState(TransportState.SETUP)) {
int version = parcel.readInt();
IBinder binder = parcel.readStrongBinder();
if (version != WIRE_FORMAT_VERSION) {
shutdownInternal(
Status.UNAVAILABLE.withDescription("Wire format version mismatch"), true);
} else if (binder == null) {
shutdownInternal(
Status.UNAVAILABLE.withDescription("Malformed SETUP_TRANSPORT data"), true);
} else {
authResultFuture = checkServerAuthorizationAsync(remoteUid);
Futures.addCallback(
authResultFuture,
new FutureCallback<Status>() {
@Override
public void onSuccess(Status result) {
handleAuthResult(binder, result);
}
@Override
public void onFailure(Throwable t) {
handleAuthResult(t);
}
},
offloadExecutor);
}
}
}
private ListenableFuture<Status> checkServerAuthorizationAsync(int remoteUid) {
return (securityPolicy instanceof AsyncSecurityPolicy)
? ((AsyncSecurityPolicy) securityPolicy).checkAuthorizationAsync(remoteUid)
: Futures.submit(() -> securityPolicy.checkAuthorization(remoteUid), offloadExecutor);
}
private synchronized void handleAuthResult(IBinder binder, Status authorization) {
if (inState(TransportState.SETUP)) {
if (!authorization.isOk()) {
shutdownInternal(authorization, true);
} else if (!setOutgoingBinder(OneWayBinderProxy.wrap(binder, offloadExecutor))) {
shutdownInternal(
Status.UNAVAILABLE.withDescription("Failed to observe outgoing binder"), true);
} else {
// Check state again, since a failure inside setOutgoingBinder (or a callback it
// triggers), could have shut us down.
if (!isShutdown()) {
setState(TransportState.READY);
attributes = clientTransportListener.filterTransport(attributes);
clientTransportListener.transportReady();
if (readyTimeoutFuture != null) {
readyTimeoutFuture.cancel(false);
readyTimeoutFuture = null;
}
}
}
}
}
private synchronized void handleAuthResult(Throwable t) {
shutdownInternal(
Status.INTERNAL.withDescription("Could not evaluate SecurityPolicy").withCause(t), true);
}
@GuardedBy("this")
@Override
protected void handlePingResponse(Parcel parcel) {
pingTracker.onPingResponse(parcel.readInt());
}
private static ClientStream newFailingClientStream(
Status failure, Attributes attributes, Metadata headers, ClientStreamTracer[] tracers) {
StatsTraceContext statsTraceContext =
StatsTraceContext.newClientContext(tracers, attributes, headers);
statsTraceContext.clientOutboundHeaders();
return new FailingClientStream(failure, tracers);
}
private static InternalLogId buildLogId(
Context sourceContext, AndroidComponentAddress targetAddress) {
return InternalLogId.allocate(
BinderClientTransport.class,
sourceContext.getClass().getSimpleName() + "->" + targetAddress);
}
private static Attributes buildClientAttributes(
Attributes eagAttrs,
Context sourceContext,
AndroidComponentAddress targetAddress,
InboundParcelablePolicy inboundParcelablePolicy) {
return Attributes.newBuilder()
.set(GrpcAttributes.ATTR_SECURITY_LEVEL, SecurityLevel.NONE) // Trust noone for now.
.set(GrpcAttributes.ATTR_CLIENT_EAG_ATTRS, eagAttrs)
.set(Grpc.TRANSPORT_ATTR_LOCAL_ADDR, AndroidComponentAddress.forContext(sourceContext))
.set(Grpc.TRANSPORT_ATTR_REMOTE_ADDR, targetAddress)
.set(INBOUND_PARCELABLE_POLICY, inboundParcelablePolicy)
.build();
}
private static Attributes setSecurityAttrs(Attributes attributes, int uid) {
return attributes.toBuilder()
.set(REMOTE_UID, uid)
.set(
GrpcAttributes.ATTR_SECURITY_LEVEL,
uid == Process.myUid()
? SecurityLevel.PRIVACY_AND_INTEGRITY
: SecurityLevel.INTEGRITY) // TODO: Have the SecrityPolicy decide this.
.build();
}
}
/** Concrete server-side transport implementation. */
@Internal
public static final class BinderServerTransport extends BinderTransport
implements ServerTransport {
private final List<ServerStreamTracer.Factory> streamTracerFactories;
@Nullable private ServerTransportListener serverTransportListener;
/**
* Constructs a new transport instance.
*
* @param binderDecorator used to decorate 'callbackBinder', for fault injection.
*/
public BinderServerTransport(
ObjectPool<ScheduledExecutorService> executorServicePool,
Attributes attributes,
List<ServerStreamTracer.Factory> streamTracerFactories,
OneWayBinderProxy.Decorator binderDecorator,
IBinder callbackBinder) {
super(executorServicePool, attributes, binderDecorator, buildLogId(attributes));
this.streamTracerFactories = streamTracerFactories;
// TODO(jdcormie): Plumb in the Server's executor() and use it here instead.
setOutgoingBinder(OneWayBinderProxy.wrap(callbackBinder, getScheduledExecutorService()));
}
public synchronized void setServerTransportListener(
ServerTransportListener serverTransportListener) {
this.serverTransportListener = serverTransportListener;
if (isShutdown()) {
setState(TransportState.SHUTDOWN_TERMINATED);
notifyTerminated();
releaseExecutors();
} else {
sendSetupTransaction();
// Check we're not shutdown again, since a failure inside sendSetupTransaction (or a
// callback it triggers), could have shut us down.
if (!isShutdown()) {
setState(TransportState.READY);
attributes = serverTransportListener.transportReady(attributes);
}
}
}
StatsTraceContext createStatsTraceContext(String methodName, Metadata headers) {
return StatsTraceContext.newServerContext(streamTracerFactories, methodName, headers);
}
synchronized Status startStream(ServerStream stream, String methodName, Metadata headers) {
if (isShutdown()) {
return Status.UNAVAILABLE.withDescription("transport is shutdown");
} else {
serverTransportListener.streamCreated(stream, methodName, headers);
return Status.OK;
}
}
@Override
@GuardedBy("this")
void notifyShutdown(Status status) {
// Nothing to do.
}
@Override
@GuardedBy("this")
void notifyTerminated() {
if (serverTransportListener != null) {
serverTransportListener.transportTerminated();
}
}
@Override
public synchronized void shutdown() {
shutdownInternal(Status.OK, false);
}
@Override
public synchronized void shutdownNow(Status reason) {
shutdownInternal(reason, true);
}
@Override
@Nullable
@GuardedBy("this")
protected Inbound<?> createInbound(int callId) {
return new Inbound.ServerInbound(this, attributes, callId);
}
private static InternalLogId buildLogId(Attributes attributes) {
return InternalLogId.allocate(
BinderServerTransport.class, "from " + attributes.get(Grpc.TRANSPORT_ATTR_REMOTE_ADDR));
}
}
private static void checkTransition(TransportState current, TransportState next) { private static void checkTransition(TransportState current, TransportState next) {
switch (next) { switch (next) {
case SETUP: case SETUP:

View File

@ -610,10 +610,9 @@ abstract class Inbound<L extends StreamListener> implements StreamListener.Messa
// Server-side inbound transactions. // Server-side inbound transactions.
static final class ServerInbound extends Inbound<ServerStreamListener> { static final class ServerInbound extends Inbound<ServerStreamListener> {
private final BinderTransport.BinderServerTransport serverTransport; private final BinderServerTransport serverTransport;
ServerInbound( ServerInbound(BinderServerTransport transport, Attributes attributes, int callId) {
BinderTransport.BinderServerTransport transport, Attributes attributes, int callId) {
super(transport, attributes, callId); super(transport, attributes, callId);
this.serverTransport = transport; this.serverTransport = transport;
} }

View File

@ -0,0 +1,299 @@
/*
* Copyright 2025 The 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.
*/
package io.grpc.binder.internal;
import static com.google.common.base.Preconditions.checkNotNull;
import static com.google.common.base.Preconditions.checkState;
import static io.grpc.binder.internal.SystemApis.createContextAsUser;
import android.annotation.SuppressLint;
import android.content.BroadcastReceiver;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.pm.PackageManager;
import android.content.pm.ResolveInfo;
import android.os.Build;
import android.os.UserHandle;
import com.google.common.collect.ImmutableMap;
import com.google.common.util.concurrent.FutureCallback;
import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture;
import com.google.common.util.concurrent.MoreExecutors;
import io.grpc.Attributes;
import io.grpc.EquivalentAddressGroup;
import io.grpc.NameResolver;
import io.grpc.Status;
import io.grpc.StatusException;
import io.grpc.StatusOr;
import io.grpc.SynchronizationContext;
import io.grpc.binder.AndroidComponentAddress;
import io.grpc.binder.ApiConstants;
import java.util.ArrayList;
import java.util.List;
import java.util.Set;
import java.util.concurrent.Executor;
import javax.annotation.Nullable;
/**
* A {@link NameResolver} that resolves Android-standard "intent:" target URIs to the list of {@link
* AndroidComponentAddress} that match it by manifest intent filter.
*/
final class IntentNameResolver extends NameResolver {
private final Intent targetIntent; // Never mutated.
@Nullable private final UserHandle targetUser; // null means same user that hosts this process.
private final Context targetUserContext;
private final Executor offloadExecutor;
private final Executor sequentialExecutor;
private final SynchronizationContext syncContext;
private final ServiceConfigParser serviceConfigParser;
// Accessed only on `sequentialExecutor`
@Nullable private PackageChangeReceiver receiver; // != null when registered
// Accessed only on 'syncContext'.
private boolean shutdown;
private boolean queryNeeded;
@Nullable private Listener2 listener; // != null after start().
@Nullable private ListenableFuture<ResolutionResult> queryResultFuture; // != null when querying.
@EquivalentAddressGroup.Attr
private static final Attributes CONSTANT_EAG_ATTRS =
Attributes.newBuilder()
// Servers discovered in PackageManager are especially untrusted. After all, any app can
// declare any intent filter it wants! Require pre-authorization so that unauthorized apps
// don't even get a chance to run onCreate()/onBind().
.set(ApiConstants.PRE_AUTH_SERVER_OVERRIDE, true)
.build();
IntentNameResolver(Intent targetIntent, Args args) {
this.targetIntent = targetIntent;
this.targetUser = args.getArg(ApiConstants.TARGET_ANDROID_USER);
Context context =
checkNotNull(args.getArg(ApiConstants.SOURCE_ANDROID_CONTEXT), "SOURCE_ANDROID_CONTEXT")
.getApplicationContext();
this.targetUserContext =
targetUser != null ? createContextForTargetUserOrThrow(context, targetUser) : context;
// This Executor is nominally optional but all grpc-java Channels provide it since 1.25.
this.offloadExecutor =
checkNotNull(args.getOffloadExecutor(), "NameResolver.Args.getOffloadExecutor()");
// Ensures start()'s work runs before resolve()'s' work, and both run before shutdown()'s.
this.sequentialExecutor = MoreExecutors.newSequentialExecutor(offloadExecutor);
this.syncContext = args.getSynchronizationContext();
this.serviceConfigParser = args.getServiceConfigParser();
}
private static Context createContextForTargetUserOrThrow(Context context, UserHandle targetUser) {
try {
return createContextAsUser(context, targetUser, /* flags= */ 0); // @SystemApi since R.
} catch (ReflectiveOperationException e) {
throw new IllegalArgumentException(
"TARGET_ANDROID_USER NameResolver.Arg requires SDK_INT >= R and @SystemApi visibility");
}
}
@Override
public void start(Listener2 listener) {
checkState(this.listener == null, "Already started!");
checkState(!shutdown, "Resolver is shutdown");
this.listener = checkNotNull(listener);
sequentialExecutor.execute(this::registerReceiver);
resolve();
}
@Override
public void refresh() {
checkState(listener != null, "Not started!");
resolve();
}
private void resolve() {
syncContext.throwIfNotInThisSynchronizationContext();
if (shutdown) {
return;
}
// We can't block here in 'syncContext' so we offload PackageManager queries to an Executor.
// But offloading complicates things a bit because other calls can arrive while we wait for the
// results. We keep 'listener' up-to-date with the latest state in PackageManager by doing:
// 1. Only one query-and-report-to-listener operation at a time.
// 2. At least one query-and-report-to-listener AFTER every PackageManager state change.
if (queryResultFuture == null) {
queryResultFuture = Futures.submit(this::queryPackageManager, sequentialExecutor);
queryResultFuture.addListener(this::onQueryComplete, syncContext);
} else {
// There's already a query in-flight but (2) says we need at least one more. Our sequential
// Executor would be enough to ensure (1) but we also don't want a backlog of work to build up
// if things change rapidly. Just make a note to start a new query when this one finishes.
queryNeeded = true;
}
}
private void onQueryComplete() {
syncContext.throwIfNotInThisSynchronizationContext();
checkState(queryResultFuture != null);
checkState(queryResultFuture.isDone());
// Capture non-final `listener` here while we're on 'syncContext'.
Listener2 listener = checkNotNull(this.listener);
Futures.addCallback(
queryResultFuture, // Already isDone() so this execute()s immediately.
new FutureCallback<ResolutionResult>() {
@Override
public void onSuccess(ResolutionResult result) {
listener.onResult2(result);
}
@Override
public void onFailure(Throwable t) {
listener.onResult2(
ResolutionResult.newBuilder()
.setAddressesOrError(StatusOr.fromStatus(Status.fromThrowable(t)))
.build());
}
},
syncContext); // Already on 'syncContext' but addCallback() is faster than try/get/catch.
queryResultFuture = null;
if (queryNeeded) {
// One or more resolve() requests arrived while we were working on the last one. Just one
// follow-on query can subsume all of them.
queryNeeded = false;
resolve();
}
}
@Override
public String getServiceAuthority() {
return "localhost";
}
@Override
public void shutdown() {
syncContext.throwIfNotInThisSynchronizationContext();
if (!shutdown) {
shutdown = true;
sequentialExecutor.execute(this::maybeUnregisterReceiver);
}
}
private ResolutionResult queryPackageManager() throws StatusException {
List<ResolveInfo> queryResults = queryIntentServices(targetIntent);
// Avoid a spurious UnsafeIntentLaunchViolation later. Since S, Android's StrictMode is very
// conservative, marking any Intent parsed from a string as suspicious and complaining when you
// bind to it. But all this is pointless with grpc-binder, which already goes even further by
// not trusting addresses at all! Instead, we rely on SecurityPolicy, which won't allow a
// connection to an unauthorized server UID no matter how you got there.
Intent prototypeBindIntent = sanitize(targetIntent);
// Model each matching android.app.Service as an EAG (server) with a single address.
List<EquivalentAddressGroup> addresses = new ArrayList<>();
for (ResolveInfo resolveInfo : queryResults) {
prototypeBindIntent.setComponent(
new ComponentName(resolveInfo.serviceInfo.packageName, resolveInfo.serviceInfo.name));
addresses.add(
new EquivalentAddressGroup(
AndroidComponentAddress.newBuilder()
.setBindIntent(prototypeBindIntent) // Makes a copy.
.setTargetUser(targetUser)
.build(),
CONSTANT_EAG_ATTRS));
}
return ResolutionResult.newBuilder()
.setAddressesOrError(StatusOr.fromValue(addresses))
// Empty service config means we get the default 'pick_first' load balancing policy.
.setServiceConfig(serviceConfigParser.parseServiceConfig(ImmutableMap.of()))
.build();
}
private List<ResolveInfo> queryIntentServices(Intent intent) throws StatusException {
int flags = 0;
if (Build.VERSION.SDK_INT >= 29) {
// Don't match direct-boot-unaware Services that can't presently be created. We'll query again
// after the user is unlocked. The MATCH_DIRECT_BOOT_AUTO behavior is actually the default but
// being explicit here avoids an android.os.strictmode.ImplicitDirectBootViolation.
flags |= PackageManager.MATCH_DIRECT_BOOT_AUTO;
}
List<ResolveInfo> intentServices =
targetUserContext.getPackageManager().queryIntentServices(intent, flags);
if (intentServices == null || intentServices.isEmpty()) {
// Must be the same as when ServiceBinding's call to bindService() returns false.
throw Status.UNIMPLEMENTED
.withDescription("Service not found for intent " + intent)
.asException();
}
return intentServices;
}
// Returns a new Intent with the same action, data and categories as 'input'.
private static Intent sanitize(Intent input) {
Intent output = new Intent();
output.setAction(input.getAction());
output.setData(input.getData());
Set<String> categories = input.getCategories();
if (categories != null) {
for (String category : categories) {
output.addCategory(category);
}
}
// Don't bother copying extras and flags since AndroidComponentAddress (rightly) ignores them.
// Don't bother copying package or ComponentName either, since we're about to set that.
return output;
}
final class PackageChangeReceiver extends BroadcastReceiver {
@Override
public void onReceive(Context context, Intent intent) {
// Get off the main thread and into the correct SynchronizationContext.
syncContext.executeLater(IntentNameResolver.this::resolve);
offloadExecutor.execute(syncContext::drain);
}
}
@SuppressLint("UnprotectedReceiver") // All of these are protected system broadcasts.
private void registerReceiver() {
checkState(receiver == null, "Already registered!");
receiver = new PackageChangeReceiver();
IntentFilter filter = new IntentFilter();
filter.addDataScheme("package");
filter.addAction(Intent.ACTION_PACKAGE_ADDED);
filter.addAction(Intent.ACTION_PACKAGE_CHANGED);
filter.addAction(Intent.ACTION_PACKAGE_REMOVED);
filter.addAction(Intent.ACTION_PACKAGE_REPLACED);
targetUserContext.registerReceiver(receiver, filter);
if (Build.VERSION.SDK_INT >= 24) {
// Clients running in direct boot mode must refresh() when the user is unlocked because
// that's when `directBootAware=false` services become visible in queryIntentServices()
// results. ACTION_BOOT_COMPLETED would work too but it's delivered with lower priority.
targetUserContext.registerReceiver(receiver, new IntentFilter(Intent.ACTION_USER_UNLOCKED));
}
}
private void maybeUnregisterReceiver() {
if (receiver != null) { // NameResolver API contract appears to allow shutdown without start().
targetUserContext.unregisterReceiver(receiver);
receiver = null;
}
}
}

View File

@ -0,0 +1,77 @@
/*
* Copyright 2025 The 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.
*/
package io.grpc.binder.internal;
import static android.content.Intent.URI_INTENT_SCHEME;
import android.content.Intent;
import com.google.common.collect.ImmutableSet;
import io.grpc.NameResolver;
import io.grpc.NameResolver.Args;
import io.grpc.NameResolverProvider;
import io.grpc.binder.AndroidComponentAddress;
import java.net.SocketAddress;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.Objects;
import javax.annotation.Nullable;
/**
* A {@link NameResolverProvider} that handles Android-standard "intent:" target URIs, resolving
* them to the list of {@link AndroidComponentAddress} that match by manifest intent filter.
*/
public final class IntentNameResolverProvider extends NameResolverProvider {
static final String ANDROID_INTENT_SCHEME = "intent";
@Override
public String getDefaultScheme() {
return ANDROID_INTENT_SCHEME;
}
@Nullable
@Override
public NameResolver newNameResolver(URI targetUri, final Args args) {
if (Objects.equals(targetUri.getScheme(), ANDROID_INTENT_SCHEME)) {
return new IntentNameResolver(parseUriArg(targetUri), args);
} else {
return null;
}
}
@Override
public boolean isAvailable() {
return true;
}
@Override
public int priority() {
return 3; // Lower than DNS so we don't accidentally become the default scheme for a registry.
}
@Override
public ImmutableSet<Class<? extends SocketAddress>> getProducedSocketAddressTypes() {
return ImmutableSet.of(AndroidComponentAddress.class);
}
private static Intent parseUriArg(URI targetUri) {
try {
return Intent.parseUri(targetUri.toString(), URI_INTENT_SCHEME);
} catch (URISyntaxException e) {
throw new IllegalArgumentException(e);
}
}
}

View File

@ -193,18 +193,27 @@ final class ServiceBinding implements Bindable, ServiceConnection {
bindResult = context.bindService(bindIntent, conn, flags); bindResult = context.bindService(bindIntent, conn, flags);
break; break;
case BIND_SERVICE_AS_USER: case BIND_SERVICE_AS_USER:
bindResult = context.bindServiceAsUser(bindIntent, conn, flags, targetUserHandle); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
bindResult = context.bindServiceAsUser(bindIntent, conn, flags, targetUserHandle);
} else {
return Status.INTERNAL.withDescription("Cross user Channel requires Android R+");
}
break; break;
case DEVICE_POLICY_BIND_SEVICE_ADMIN: case DEVICE_POLICY_BIND_SEVICE_ADMIN:
DevicePolicyManager devicePolicyManager = DevicePolicyManager devicePolicyManager =
(DevicePolicyManager) context.getSystemService(Context.DEVICE_POLICY_SERVICE); (DevicePolicyManager) context.getSystemService(Context.DEVICE_POLICY_SERVICE);
bindResult = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
devicePolicyManager.bindDeviceAdminServiceAsUser( bindResult =
channelCredentials.getDevicePolicyAdminComponentName(), devicePolicyManager.bindDeviceAdminServiceAsUser(
bindIntent, channelCredentials.getDevicePolicyAdminComponentName(),
conn, bindIntent,
flags, conn,
targetUserHandle); flags,
targetUserHandle);
} else {
return Status.INTERNAL.withDescription(
"Device policy admin binding requires Android R+");
}
break; break;
} }
if (!bindResult) { if (!bindResult) {

View File

@ -0,0 +1,60 @@
/*
* Copyright 2025 The 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.
*/
package io.grpc.binder.internal;
import android.content.Context;
import android.os.UserHandle;
import java.lang.reflect.Method;
/**
* A collection of static methods that wrap hidden Android "System APIs."
*
* <p>grpc-java can't call Android methods marked @SystemApi directly, even though many of our users
* are "system apps" entitled to do so. Being a library built outside the Android source tree, these
* "non-SDK" elements simply don't exist from our compiler's perspective. Instead we resort to
* reflection but use the static wrappers found here to keep call sites readable and type safe.
*
* <p>Modern Android's JRE also limits the visibility of these methods at *runtime*. Only certain
* privileged apps installed on the system image app can call them, even using reflection, and this
* wrapper doesn't change that. Callers are responsible for ensuring that the host app actually has
* the ability to call @SystemApis and all methods throw {@link ReflectiveOperationException} as a
* reminder to do that. See
* https://developer.android.com/guide/app-compatibility/restrictions-non-sdk-interfaces for more.
*/
final class SystemApis {
private static volatile Method createContextAsUserMethod;
// Not to be instantiated.
private SystemApis() {}
/**
* Returns a new Context object whose methods act as if they were running in the given user.
*
* @throws ReflectiveOperationException if SDK_INT < R or host app lacks @SystemApi visibility
*/
public static Context createContextAsUser(Context context, UserHandle userHandle, int flags)
throws ReflectiveOperationException {
if (createContextAsUserMethod == null) {
synchronized (SystemApis.class) {
if (createContextAsUserMethod == null) {
createContextAsUserMethod =
Context.class.getMethod("createContextAsUser", UserHandle.class, int.class);
}
}
}
return (Context) createContextAsUserMethod.invoke(context, userHandle, flags);
}
}

View File

@ -56,12 +56,12 @@ public final class BinderServerTransportTest {
@Mock IBinder mockBinder; @Mock IBinder mockBinder;
BinderTransport.BinderServerTransport transport; BinderServerTransport transport;
@Before @Before
public void setUp() throws Exception { public void setUp() throws Exception {
transport = transport =
new BinderTransport.BinderServerTransport( new BinderServerTransport(
new FixedObjectPool<>(executorService), new FixedObjectPool<>(executorService),
Attributes.EMPTY, Attributes.EMPTY,
ImmutableList.of(), ImmutableList.of(),

View File

@ -0,0 +1,115 @@
/*
* Copyright 2025 The 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.
*/
package io.grpc.binder.internal;
import static android.os.Looper.getMainLooper;
import static com.google.common.truth.Truth.assertThat;
import static org.junit.Assert.assertThrows;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import static org.robolectric.Shadows.shadowOf;
import android.app.Application;
import androidx.core.content.ContextCompat;
import androidx.test.core.app.ApplicationProvider;
import io.grpc.NameResolver;
import io.grpc.NameResolver.ResolutionResult;
import io.grpc.NameResolver.ServiceConfigParser;
import io.grpc.NameResolverProvider;
import io.grpc.SynchronizationContext;
import io.grpc.binder.ApiConstants;
import java.net.URI;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.ArgumentCaptor;
import org.mockito.Captor;
import org.mockito.Mock;
import org.mockito.junit.MockitoJUnit;
import org.mockito.junit.MockitoTestRule;
import org.robolectric.RobolectricTestRunner;
/** A test for IntentNameResolverProvider. */
@RunWith(RobolectricTestRunner.class)
public final class IntentNameResolverProviderTest {
private final Application appContext = ApplicationProvider.getApplicationContext();
private final SynchronizationContext syncContext = newSynchronizationContext();
private final NameResolver.Args args = newNameResolverArgs();
private NameResolverProvider provider;
@Rule public MockitoTestRule mockitoTestRule = MockitoJUnit.testRule(this);
@Mock public NameResolver.Listener2 mockListener;
@Captor public ArgumentCaptor<ResolutionResult> resultCaptor;
@Before
public void setUp() {
provider = new IntentNameResolverProvider();
}
@Test
public void testProviderScheme_returnsIntentScheme() throws Exception {
assertThat(provider.getDefaultScheme())
.isEqualTo(IntentNameResolverProvider.ANDROID_INTENT_SCHEME);
}
@Test
public void testNoResolverForUnknownScheme_returnsNull() throws Exception {
assertThat(provider.newNameResolver(new URI("random://uri"), args)).isNull();
}
@Test
public void testResolutionWithBadUri_throwsIllegalArg() throws Exception {
assertThrows(
IllegalArgumentException.class,
() -> provider.newNameResolver(new URI("intent:xxx#Intent;e.x=1;end;"), args));
}
@Test
public void testResolverForIntentScheme_returnsResolver() throws Exception {
URI uri = new URI("intent://authority/path#Intent;action=action;scheme=scheme;end");
NameResolver resolver = provider.newNameResolver(uri, args);
assertThat(resolver).isNotNull();
assertThat(resolver.getServiceAuthority()).isEqualTo("localhost");
syncContext.execute(() -> resolver.start(mockListener));
shadowOf(getMainLooper()).idle();
verify(mockListener).onResult2(resultCaptor.capture());
assertThat(resultCaptor.getValue().getAddressesOrError()).isNotNull();
syncContext.execute(resolver::shutdown);
shadowOf(getMainLooper()).idle();
}
/** Returns a new test-specific {@link NameResolver.Args} instance. */
private NameResolver.Args newNameResolverArgs() {
return NameResolver.Args.newBuilder()
.setDefaultPort(-1)
.setProxyDetector((target) -> null) // No proxies here.
.setSynchronizationContext(syncContext)
.setOffloadExecutor(ContextCompat.getMainExecutor(appContext))
.setServiceConfigParser(mock(ServiceConfigParser.class))
.setArg(ApiConstants.SOURCE_ANDROID_CONTEXT, appContext)
.build();
}
private static SynchronizationContext newSynchronizationContext() {
return new SynchronizationContext(
(thread, exception) -> {
throw new AssertionError(exception);
});
}
}

View File

@ -0,0 +1,531 @@
/*
* Copyright 2025 The 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.
*/
package io.grpc.binder.internal;
import static android.content.Intent.ACTION_PACKAGE_ADDED;
import static android.content.Intent.ACTION_PACKAGE_REPLACED;
import static android.os.Looper.getMainLooper;
import static android.os.Process.myUserHandle;
import static com.google.common.collect.ImmutableList.toImmutableList;
import static com.google.common.truth.Truth.assertThat;
import static org.junit.Assert.assertThrows;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.doThrow;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyNoMoreInteractions;
import static org.robolectric.Shadows.shadowOf;
import android.app.Application;
import android.content.ComponentName;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.pm.ServiceInfo;
import android.net.Uri;
import android.os.UserHandle;
import android.os.UserManager;
import androidx.annotation.NonNull;
import androidx.core.content.ContextCompat;
import androidx.test.core.app.ApplicationProvider;
import com.google.common.collect.ImmutableList;
import io.grpc.EquivalentAddressGroup;
import io.grpc.NameResolver;
import io.grpc.NameResolver.ResolutionResult;
import io.grpc.NameResolver.ServiceConfigParser;
import io.grpc.Status;
import io.grpc.StatusOr;
import io.grpc.SynchronizationContext;
import io.grpc.binder.AndroidComponentAddress;
import io.grpc.binder.ApiConstants;
import java.lang.Thread.UncaughtExceptionHandler;
import java.net.SocketAddress;
import java.util.ArrayList;
import java.util.List;
import java.util.Set;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.ArgumentCaptor;
import org.mockito.Captor;
import org.mockito.Mock;
import org.mockito.junit.MockitoJUnit;
import org.mockito.junit.MockitoTestRule;
import org.robolectric.RobolectricTestRunner;
import org.robolectric.annotation.Config;
import org.robolectric.shadows.ShadowPackageManager;
/** A test for IntentNameResolverProvider. */
@RunWith(RobolectricTestRunner.class)
public final class IntentNameResolverTest {
private static final ComponentName SOME_COMPONENT_NAME =
new ComponentName("com.foo.bar", "SomeComponent");
private static final ComponentName ANOTHER_COMPONENT_NAME =
new ComponentName("org.blah", "AnotherComponent");
private final Application appContext = ApplicationProvider.getApplicationContext();
private final SynchronizationContext syncContext = newSynchronizationContext();
private final NameResolver.Args args = newNameResolverArgs().build();
private final ShadowPackageManager shadowPackageManager =
shadowOf(appContext.getPackageManager());
@Rule public MockitoTestRule mockitoTestRule = MockitoJUnit.testRule(this);
@Mock public NameResolver.Listener2 mockListener;
@Captor public ArgumentCaptor<ResolutionResult> resultCaptor;
@Test
public void testResolverForIntentScheme_returnsResolverWithLocalHostAuthority() throws Exception {
NameResolver resolver = newNameResolver(newIntent());
assertThat(resolver).isNotNull();
assertThat(resolver.getServiceAuthority()).isEqualTo("localhost");
}
@Test
public void testResolutionWithoutServicesAvailable_returnsUnimplemented() throws Exception {
NameResolver nameResolver = newNameResolver(newIntent());
syncContext.execute(() -> nameResolver.start(mockListener));
shadowOf(getMainLooper()).idle();
verify(mockListener).onResult2(resultCaptor.capture());
assertThat(resultCaptor.getValue().getAddressesOrError().getStatus().getCode())
.isEqualTo(Status.UNIMPLEMENTED.getCode());
}
@Test
public void testResolutionWithMultipleServicesAvailable_returnsAndroidComponentAddresses()
throws Exception {
Intent intent = newIntent();
IntentFilter serviceIntentFilter = newFilterMatching(intent);
shadowPackageManager.addServiceIfNotPresent(SOME_COMPONENT_NAME);
shadowPackageManager.addIntentFilterForService(SOME_COMPONENT_NAME, serviceIntentFilter);
// Adds another valid Service
shadowPackageManager.addServiceIfNotPresent(ANOTHER_COMPONENT_NAME);
shadowPackageManager.addIntentFilterForService(ANOTHER_COMPONENT_NAME, serviceIntentFilter);
NameResolver nameResolver = newNameResolver(intent);
syncContext.execute(() -> nameResolver.start(mockListener));
shadowOf(getMainLooper()).idle();
verify(mockListener, never()).onError(any());
verify(mockListener).onResult2(resultCaptor.capture());
assertThat(getAddressesOrThrow(resultCaptor.getValue()))
.containsExactly(
toAddressList(intent.cloneFilter().setComponent(SOME_COMPONENT_NAME)),
toAddressList(intent.cloneFilter().setComponent(ANOTHER_COMPONENT_NAME)));
syncContext.execute(nameResolver::shutdown);
shadowOf(getMainLooper()).idle();
}
@Test
public void testExplicitResolutionByComponent_returnsRestrictedResults() throws Exception {
Intent intent = newIntent();
IntentFilter serviceIntentFilter = newFilterMatching(intent);
shadowPackageManager.addServiceIfNotPresent(SOME_COMPONENT_NAME);
shadowPackageManager.addIntentFilterForService(SOME_COMPONENT_NAME, serviceIntentFilter);
shadowPackageManager.addServiceIfNotPresent(ANOTHER_COMPONENT_NAME);
shadowPackageManager.addIntentFilterForService(ANOTHER_COMPONENT_NAME, serviceIntentFilter);
NameResolver nameResolver =
newNameResolver(intent.cloneFilter().setComponent(ANOTHER_COMPONENT_NAME));
syncContext.execute(() -> nameResolver.start(mockListener));
shadowOf(getMainLooper()).idle();
verify(mockListener, never()).onError(any());
verify(mockListener).onResult2(resultCaptor.capture());
assertThat(getAddressesOrThrow(resultCaptor.getValue()))
.containsExactly(toAddressList(intent.cloneFilter().setComponent(ANOTHER_COMPONENT_NAME)));
syncContext.execute(nameResolver::shutdown);
shadowOf(getMainLooper()).idle();
}
@Test
public void testExplicitResolutionByPackage_returnsRestrictedResults() throws Exception {
Intent intent = newIntent();
IntentFilter serviceIntentFilter = newFilterMatching(intent);
shadowPackageManager.addServiceIfNotPresent(SOME_COMPONENT_NAME);
shadowPackageManager.addIntentFilterForService(SOME_COMPONENT_NAME, serviceIntentFilter);
shadowPackageManager.addServiceIfNotPresent(ANOTHER_COMPONENT_NAME);
shadowPackageManager.addIntentFilterForService(ANOTHER_COMPONENT_NAME, serviceIntentFilter);
NameResolver nameResolver =
newNameResolver(intent.cloneFilter().setPackage(ANOTHER_COMPONENT_NAME.getPackageName()));
syncContext.execute(() -> nameResolver.start(mockListener));
shadowOf(getMainLooper()).idle();
verify(mockListener, never()).onError(any());
verify(mockListener).onResult2(resultCaptor.capture());
assertThat(getAddressesOrThrow(resultCaptor.getValue()))
.containsExactly(toAddressList(intent.cloneFilter().setComponent(ANOTHER_COMPONENT_NAME)));
syncContext.execute(nameResolver::shutdown);
shadowOf(getMainLooper()).idle();
}
@Test
public void testResolution_setsPreAuthEagAttribute() throws Exception {
Intent intent = newIntent();
IntentFilter serviceIntentFilter = newFilterMatching(intent);
shadowPackageManager.addServiceIfNotPresent(SOME_COMPONENT_NAME);
shadowPackageManager.addIntentFilterForService(SOME_COMPONENT_NAME, serviceIntentFilter);
NameResolver nameResolver = newNameResolver(intent);
syncContext.execute(() -> nameResolver.start(mockListener));
shadowOf(getMainLooper()).idle();
verify(mockListener).onResult2(resultCaptor.capture());
assertThat(getAddressesOrThrow(resultCaptor.getValue()))
.containsExactly(toAddressList(intent.cloneFilter().setComponent(SOME_COMPONENT_NAME)));
assertThat(
getEagsOrThrow(resultCaptor.getValue()).stream()
.map(EquivalentAddressGroup::getAttributes)
.collect(toImmutableList())
.get(0)
.get(ApiConstants.PRE_AUTH_SERVER_OVERRIDE))
.isTrue();
syncContext.execute(nameResolver::shutdown);
shadowOf(getMainLooper()).idle();
}
@Test
public void testServiceRemoved_pushesUpdatedAndroidComponentAddresses() throws Exception {
Intent intent = newIntent();
IntentFilter serviceIntentFilter = newFilterMatching(intent);
shadowPackageManager.addServiceIfNotPresent(SOME_COMPONENT_NAME);
shadowPackageManager.addIntentFilterForService(SOME_COMPONENT_NAME, serviceIntentFilter);
shadowPackageManager.addServiceIfNotPresent(ANOTHER_COMPONENT_NAME);
shadowPackageManager.addIntentFilterForService(ANOTHER_COMPONENT_NAME, serviceIntentFilter);
NameResolver nameResolver = newNameResolver(intent);
syncContext.execute(() -> nameResolver.start(mockListener));
shadowOf(getMainLooper()).idle();
verify(mockListener, never()).onError(any());
verify(mockListener).onResult2(resultCaptor.capture());
assertThat(getAddressesOrThrow(resultCaptor.getValue()))
.containsExactly(
toAddressList(intent.cloneFilter().setComponent(SOME_COMPONENT_NAME)),
toAddressList(intent.cloneFilter().setComponent(ANOTHER_COMPONENT_NAME)));
shadowPackageManager.removeService(ANOTHER_COMPONENT_NAME);
broadcastPackageChange(ACTION_PACKAGE_REPLACED, ANOTHER_COMPONENT_NAME.getPackageName());
shadowOf(getMainLooper()).idle();
verify(mockListener, never()).onError(any());
verify(mockListener, times(2)).onResult2(resultCaptor.capture());
assertThat(getAddressesOrThrow(resultCaptor.getValue()))
.containsExactly(toAddressList(intent.cloneFilter().setComponent(SOME_COMPONENT_NAME)));
syncContext.execute(nameResolver::shutdown);
shadowOf(getMainLooper()).idle();
verifyNoMoreInteractions(mockListener);
assertThat(shadowOf(appContext).getRegisteredReceivers()).isEmpty();
}
@Test
@Config(sdk = 30)
public void testTargetAndroidUser_pushesUpdatedAddresses() throws Exception {
Intent intent = newIntent();
IntentFilter serviceIntentFilter = newFilterMatching(intent);
NameResolver nameResolver =
newNameResolver(
intent,
newNameResolverArgs().setArg(ApiConstants.TARGET_ANDROID_USER, myUserHandle()).build());
syncContext.execute(() -> nameResolver.start(mockListener));
shadowOf(getMainLooper()).idle();
verify(mockListener).onResult2(resultCaptor.capture());
assertThat(resultCaptor.getValue().getAddressesOrError().getStatus().getCode())
.isEqualTo(Status.UNIMPLEMENTED.getCode());
shadowPackageManager.addServiceIfNotPresent(SOME_COMPONENT_NAME);
shadowPackageManager.addIntentFilterForService(SOME_COMPONENT_NAME, serviceIntentFilter);
broadcastPackageChange(ACTION_PACKAGE_ADDED, SOME_COMPONENT_NAME.getPackageName());
shadowOf(getMainLooper()).idle();
verify(mockListener, never()).onError(any());
verify(mockListener, times(2)).onResult2(resultCaptor.capture());
assertThat(getAddressesOrThrow(resultCaptor.getValue()))
.containsExactly(
ImmutableList.of(
AndroidComponentAddress.newBuilder()
.setTargetUser(myUserHandle())
.setBindIntent(intent.cloneFilter().setComponent(SOME_COMPONENT_NAME))
.build()));
syncContext.execute(nameResolver::shutdown);
shadowOf(getMainLooper()).idle();
verifyNoMoreInteractions(mockListener);
assertThat(shadowOf(appContext).getRegisteredReceivers()).isEmpty();
}
@Test
@Config(sdk = 29)
public void testTargetAndroidUser_notSupported_throwsWithHelpfulMessage() throws Exception {
NameResolver.Args args =
newNameResolverArgs().setArg(ApiConstants.TARGET_ANDROID_USER, myUserHandle()).build();
IllegalArgumentException iae =
assertThrows(IllegalArgumentException.class, () -> newNameResolver(newIntent(), args));
assertThat(iae.getMessage()).contains("TARGET_ANDROID_USER");
assertThat(iae.getMessage()).contains("SDK_INT >= R");
}
@Test
@Config(sdk = 29)
public void testServiceAppearsUponBootComplete_pushesUpdatedAndroidComponentAddresses()
throws Exception {
Intent intent = newIntent();
IntentFilter serviceIntentFilter = newFilterMatching(intent);
// Suppose this directBootAware=true Service appears in PackageManager before a user unlock.
shadowOf(appContext.getSystemService(UserManager.class)).setUserUnlocked(false);
ServiceInfo someServiceInfo = shadowPackageManager.addServiceIfNotPresent(SOME_COMPONENT_NAME);
someServiceInfo.directBootAware = true;
shadowPackageManager.addIntentFilterForService(SOME_COMPONENT_NAME, serviceIntentFilter);
NameResolver nameResolver = newNameResolver(intent);
syncContext.execute(() -> nameResolver.start(mockListener));
shadowOf(getMainLooper()).idle();
verify(mockListener, never()).onError(any());
verify(mockListener).onResult2(resultCaptor.capture());
assertThat(getAddressesOrThrow(resultCaptor.getValue()))
.containsExactly(toAddressList(intent.cloneFilter().setComponent(SOME_COMPONENT_NAME)));
// TODO(b/331618070): Robolectric doesn't yet support ServiceInfo.directBootAware filtering.
// Simulate support by waiting for a user unlock to add this !directBootAware Service.
ServiceInfo anotherServiceInfo =
shadowPackageManager.addServiceIfNotPresent(ANOTHER_COMPONENT_NAME);
anotherServiceInfo.directBootAware = false;
shadowPackageManager.addIntentFilterForService(ANOTHER_COMPONENT_NAME, serviceIntentFilter);
shadowOf(appContext.getSystemService(UserManager.class)).setUserUnlocked(true);
broadcastUserUnlocked(myUserHandle());
shadowOf(getMainLooper()).idle();
verify(mockListener, never()).onError(any());
verify(mockListener, times(2)).onResult2(resultCaptor.capture());
assertThat(getAddressesOrThrow(resultCaptor.getValue()))
.containsExactly(
toAddressList(intent.cloneFilter().setComponent(SOME_COMPONENT_NAME)),
toAddressList(intent.cloneFilter().setComponent(ANOTHER_COMPONENT_NAME)));
syncContext.execute(nameResolver::shutdown);
shadowOf(getMainLooper()).idle();
verifyNoMoreInteractions(mockListener);
}
@Test
public void testRefresh_returnsSameAndroidComponentAddresses() throws Exception {
Intent intent = newIntent();
IntentFilter serviceIntentFilter = newFilterMatching(intent);
shadowPackageManager.addServiceIfNotPresent(SOME_COMPONENT_NAME);
shadowPackageManager.addIntentFilterForService(SOME_COMPONENT_NAME, serviceIntentFilter);
shadowPackageManager.addServiceIfNotPresent(ANOTHER_COMPONENT_NAME);
shadowPackageManager.addIntentFilterForService(ANOTHER_COMPONENT_NAME, serviceIntentFilter);
NameResolver nameResolver = newNameResolver(intent);
syncContext.execute(() -> nameResolver.start(mockListener));
shadowOf(getMainLooper()).idle();
verify(mockListener, never()).onError(any());
verify(mockListener).onResult2(resultCaptor.capture());
assertThat(getAddressesOrThrow(resultCaptor.getValue()))
.containsExactly(
toAddressList(intent.cloneFilter().setComponent(SOME_COMPONENT_NAME)),
toAddressList(intent.cloneFilter().setComponent(ANOTHER_COMPONENT_NAME)));
syncContext.execute(nameResolver::refresh);
shadowOf(getMainLooper()).idle();
verify(mockListener, never()).onError(any());
verify(mockListener, times(2)).onResult2(resultCaptor.capture());
assertThat(getAddressesOrThrow(resultCaptor.getValue()))
.containsExactly(
toAddressList(intent.cloneFilter().setComponent(SOME_COMPONENT_NAME)),
toAddressList(intent.cloneFilter().setComponent(ANOTHER_COMPONENT_NAME)));
syncContext.execute(nameResolver::shutdown);
shadowOf(getMainLooper()).idle();
assertThat(shadowOf(appContext).getRegisteredReceivers()).isEmpty();
}
@Test
public void testRefresh_collapsesMultipleRequestsIntoOneLookup() throws Exception {
Intent intent = newIntent();
IntentFilter serviceIntentFilter = newFilterMatching(intent);
shadowPackageManager.addServiceIfNotPresent(SOME_COMPONENT_NAME);
shadowPackageManager.addIntentFilterForService(SOME_COMPONENT_NAME, serviceIntentFilter);
NameResolver nameResolver = newNameResolver(intent);
syncContext.execute(() -> nameResolver.start(mockListener)); // Should kick off the 1st lookup.
syncContext.execute(nameResolver::refresh); // Should queue a lookup to run when 1st finishes.
syncContext.execute(nameResolver::refresh); // Should be ignored since a lookup is already Q'd.
syncContext.execute(nameResolver::refresh); // Also ignored.
shadowOf(getMainLooper()).idle();
verify(mockListener, never()).onError(any());
verify(mockListener, times(2)).onResult2(resultCaptor.capture());
assertThat(getAddressesOrThrow(resultCaptor.getValue()))
.containsExactly(toAddressList(intent.cloneFilter().setComponent(SOME_COMPONENT_NAME)));
syncContext.execute(nameResolver::shutdown);
shadowOf(getMainLooper()).idle();
}
private void broadcastPackageChange(String action, String pkgName) {
Intent broadcast = new Intent();
broadcast.setAction(action);
broadcast.setData(Uri.parse("package:" + pkgName));
appContext.sendBroadcast(broadcast);
}
private void broadcastUserUnlocked(UserHandle userHandle) {
Intent unlockedBroadcast = new Intent(Intent.ACTION_USER_UNLOCKED);
unlockedBroadcast.putExtra(Intent.EXTRA_USER, userHandle);
appContext.sendBroadcast(unlockedBroadcast);
}
@Test
public void testResolutionOnResultThrows_onErrorNotCalled() throws Exception {
RetainingUncaughtExceptionHandler exceptionHandler = new RetainingUncaughtExceptionHandler();
SynchronizationContext syncContext = new SynchronizationContext(exceptionHandler);
Intent intent = newIntent();
shadowPackageManager.addServiceIfNotPresent(SOME_COMPONENT_NAME);
shadowPackageManager.addIntentFilterForService(SOME_COMPONENT_NAME, newFilterMatching(intent));
@SuppressWarnings("serial")
class SomeRuntimeException extends RuntimeException {}
doThrow(SomeRuntimeException.class).when(mockListener).onResult2(any());
NameResolver nameResolver =
newNameResolver(
intent, newNameResolverArgs().setSynchronizationContext(syncContext).build());
syncContext.execute(() -> nameResolver.start(mockListener));
shadowOf(getMainLooper()).idle();
verify(mockListener).onResult2(any());
verify(mockListener, never()).onError(any());
assertThat(exceptionHandler.uncaught).hasSize(1);
assertThat(exceptionHandler.uncaught.get(0)).isInstanceOf(SomeRuntimeException.class);
}
private static Intent newIntent() {
Intent intent = new Intent();
intent.setAction("test.action");
intent.setData(Uri.parse("grpc:ServiceName"));
return intent;
}
private static IntentFilter newFilterMatching(Intent intent) {
IntentFilter filter = new IntentFilter();
if (intent.getAction() != null) {
filter.addAction(intent.getAction());
}
Uri data = intent.getData();
if (data != null) {
if (data.getScheme() != null) {
filter.addDataScheme(data.getScheme());
}
if (data.getSchemeSpecificPart() != null) {
filter.addDataSchemeSpecificPart(data.getSchemeSpecificPart(), 0);
}
}
Set<String> categories = intent.getCategories();
if (categories != null) {
for (String category : categories) {
filter.addCategory(category);
}
}
return filter;
}
private static List<EquivalentAddressGroup> getEagsOrThrow(ResolutionResult result) {
StatusOr<List<EquivalentAddressGroup>> eags = result.getAddressesOrError();
if (!eags.hasValue()) {
throw eags.getStatus().asRuntimeException();
}
return eags.getValue();
}
// Extracts just the addresses from 'result's EquivalentAddressGroups.
private static ImmutableList<List<SocketAddress>> getAddressesOrThrow(ResolutionResult result) {
return getEagsOrThrow(result).stream()
.map(EquivalentAddressGroup::getAddresses)
.collect(toImmutableList());
}
// Converts given Intents to a list of ACAs, for convenient comparison with getAddressesOrThrow().
private static ImmutableList<AndroidComponentAddress> toAddressList(Intent... bindIntents) {
ImmutableList.Builder<AndroidComponentAddress> builder = ImmutableList.builder();
for (Intent bindIntent : bindIntents) {
builder.add(AndroidComponentAddress.forBindIntent(bindIntent));
}
return builder.build();
}
private NameResolver newNameResolver(Intent targetIntent) {
return newNameResolver(targetIntent, args);
}
private NameResolver newNameResolver(Intent targetIntent, NameResolver.Args args) {
return new IntentNameResolver(targetIntent, args);
}
/** Returns a new test-specific {@link NameResolver.Args} instance. */
private NameResolver.Args.Builder newNameResolverArgs() {
return NameResolver.Args.newBuilder()
.setDefaultPort(-1)
.setProxyDetector((target) -> null) // No proxies here.
.setSynchronizationContext(syncContext)
.setOffloadExecutor(ContextCompat.getMainExecutor(appContext))
.setArg(ApiConstants.SOURCE_ANDROID_CONTEXT, appContext)
.setServiceConfigParser(mock(ServiceConfigParser.class));
}
/**
* Returns a test {@link SynchronizationContext}.
*
* <p>Exceptions will cause the test to fail with {@link AssertionError}.
*/
private static SynchronizationContext newSynchronizationContext() {
return new SynchronizationContext(
(thread, exception) -> {
throw new AssertionError(exception);
});
}
static final class RetainingUncaughtExceptionHandler implements UncaughtExceptionHandler {
final ArrayList<Throwable> uncaught = new ArrayList<>();
@Override
public void uncaughtException(@NonNull Thread t, @NonNull Throwable e) {
uncaught.add(e);
}
}
}

View File

@ -29,6 +29,7 @@ import android.content.ComponentName;
import android.content.Context; import android.content.Context;
import android.content.Intent; import android.content.Intent;
import android.content.pm.ServiceInfo; import android.content.pm.ServiceInfo;
import android.os.Build;
import android.os.IBinder; import android.os.IBinder;
import android.os.Parcel; import android.os.Parcel;
import android.os.UserHandle; import android.os.UserHandle;
@ -327,6 +328,50 @@ public final class ServiceBindingTest {
assertThat(statusException.getStatus().getDescription()).contains("12345"); assertThat(statusException.getStatus().getDescription()).contains("12345");
} }
@Test
@Config(sdk = 30)
public void testBindService_doesNotThrowInternalErrorWhenSdkAtLeastR() {
UserHandle userHandle = generateUserHandle(/* userId= */ 12345);
binding = newBuilder().setTargetUserHandle(userHandle).build();
binding.bind();
shadowOf(getMainLooper()).idle();
assertThat(Build.VERSION.SDK_INT).isEqualTo(Build.VERSION_CODES.R);
assertThat(observer.unboundReason).isNull();
}
@Test
@Config(sdk = 28)
public void testBindServiceAsUser_returnsErrorWhenSdkBelowR() {
UserHandle userHandle = generateUserHandle(/* userId= */ 12345);
binding = newBuilder().setTargetUserHandle(userHandle).build();
binding.bind();
shadowOf(getMainLooper()).idle();
assertThat(observer.unboundReason.getCode()).isEqualTo(Code.INTERNAL);
assertThat(observer.unboundReason.getDescription())
.isEqualTo("Cross user Channel requires Android R+");
}
@Test
@Config(sdk = 28)
public void testDevicePolicyBlind_returnsErrorWhenSdkBelowR() {
String deviceAdminClassName = "DevicePolicyAdmin";
ComponentName adminComponent = new ComponentName(appContext, deviceAdminClassName);
allowBindDeviceAdminForUser(appContext, adminComponent, 10);
binding =
newBuilder()
.setTargetUserHandle(UserHandle.getUserHandleForUid(10))
.setChannelCredentials(BinderChannelCredentials.forDevicePolicyAdmin(adminComponent))
.build();
binding.bind();
shadowOf(getMainLooper()).idle();
assertThat(observer.unboundReason.getCode()).isEqualTo(Code.INTERNAL);
assertThat(observer.unboundReason.getDescription())
.isEqualTo("Device policy admin binding requires Android R+");
}
@Test @Test
@Config(sdk = 30) @Config(sdk = 30)
public void testBindWithDeviceAdmin() throws Exception { public void testBindWithDeviceAdmin() throws Exception {

View File

@ -24,8 +24,8 @@ import io.grpc.internal.TestUtils.NoopChannelLogger;
import java.net.SocketAddress; import java.net.SocketAddress;
/** /**
* Helps unit tests create {@link BinderTransport.BinderClientTransport} instances without having to * Helps unit tests create {@link BinderClientTransport} instances without having to mention
* mention irrelevant details (go/tott/719). * irrelevant details (go/tott/719).
*/ */
public class BinderClientTransportBuilder { public class BinderClientTransportBuilder {
private BinderClientTransportFactory factory; private BinderClientTransportFactory factory;
@ -54,7 +54,7 @@ public class BinderClientTransportBuilder {
return this; return this;
} }
public BinderTransport.BinderClientTransport build() { public BinderClientTransport build() {
return factory.newClientTransport( return factory.newClientTransport(
checkNotNull(serverAddress), checkNotNull(options), checkNotNull(channelLogger)); checkNotNull(serverAddress), checkNotNull(options), checkNotNull(channelLogger));
} }

View File

@ -21,7 +21,7 @@ subprojects {
apply plugin: "net.ltgt.errorprone" apply plugin: "net.ltgt.errorprone"
group = "io.grpc" group = "io.grpc"
version = "1.75.0-SNAPSHOT" // CURRENT_GRPC_VERSION version = "1.76.0-SNAPSHOT" // CURRENT_GRPC_VERSION
repositories { repositories {
maven { // The google mirror is less flaky than mavenCentral() maven { // The google mirror is less flaky than mavenCentral()

View File

@ -8,7 +8,7 @@ import static io.grpc.MethodDescriptor.generateFullMethodName;
* </pre> * </pre>
*/ */
@javax.annotation.Generated( @javax.annotation.Generated(
value = "by gRPC proto compiler (version 1.75.0-SNAPSHOT)", value = "by gRPC proto compiler (version 1.76.0-SNAPSHOT)",
comments = "Source: grpc/testing/compiler/test.proto") comments = "Source: grpc/testing/compiler/test.proto")
@io.grpc.stub.annotations.GrpcGenerated @io.grpc.stub.annotations.GrpcGenerated
@java.lang.Deprecated @java.lang.Deprecated

View File

@ -8,7 +8,7 @@ import static io.grpc.MethodDescriptor.generateFullMethodName;
* </pre> * </pre>
*/ */
@javax.annotation.Generated( @javax.annotation.Generated(
value = "by gRPC proto compiler (version 1.75.0-SNAPSHOT)", value = "by gRPC proto compiler (version 1.76.0-SNAPSHOT)",
comments = "Source: grpc/testing/compiler/test.proto") comments = "Source: grpc/testing/compiler/test.proto")
@io.grpc.stub.annotations.GrpcGenerated @io.grpc.stub.annotations.GrpcGenerated
public final class TestServiceGrpc { public final class TestServiceGrpc {

View File

@ -219,7 +219,7 @@ public final class GrpcUtil {
public static final Splitter ACCEPT_ENCODING_SPLITTER = Splitter.on(',').trimResults(); public static final Splitter ACCEPT_ENCODING_SPLITTER = Splitter.on(',').trimResults();
public static final String IMPLEMENTATION_VERSION = "1.75.0-SNAPSHOT"; // CURRENT_GRPC_VERSION public static final String IMPLEMENTATION_VERSION = "1.76.0-SNAPSHOT"; // CURRENT_GRPC_VERSION
/** /**
* The default timeout in nanos for a keepalive ping request. * The default timeout in nanos for a keepalive ping request.

View File

@ -14,7 +14,7 @@ android {
namespace = 'io.grpc.cronet' namespace = 'io.grpc.cronet'
compileSdkVersion 33 compileSdkVersion 33
defaultConfig { defaultConfig {
minSdkVersion 21 minSdkVersion 22
targetSdkVersion 33 targetSdkVersion 33
versionCode 1 versionCode 1
versionName "1.0" versionName "1.0"

View File

@ -1,5 +1,4 @@
bazel_dep(name = "googleapis", repo_name = "com_google_googleapis", version = "0.0.0-20240326-1c8d509c5") bazel_dep(name = "grpc-java", repo_name = "io_grpc_grpc_java", version = "1.76.0-SNAPSHOT") # CURRENT_GRPC_VERSION
bazel_dep(name = "grpc-java", repo_name = "io_grpc_grpc_java", version = "1.75.0-SNAPSHOT") # CURRENT_GRPC_VERSION
bazel_dep(name = "grpc-proto", repo_name = "io_grpc_grpc_proto", version = "0.0.0-20240627-ec30f58") bazel_dep(name = "grpc-proto", repo_name = "io_grpc_grpc_proto", version = "0.0.0-20240627-ec30f58")
bazel_dep(name = "protobuf", repo_name = "com_google_protobuf", version = "23.1") bazel_dep(name = "protobuf", repo_name = "com_google_protobuf", version = "23.1")
bazel_dep(name = "rules_jvm_external", version = "6.0") bazel_dep(name = "rules_jvm_external", version = "6.0")

View File

@ -34,7 +34,7 @@ android {
protobuf { protobuf {
protoc { artifact = 'com.google.protobuf:protoc:3.25.1' } protoc { artifact = 'com.google.protobuf:protoc:3.25.1' }
plugins { plugins {
grpc { artifact = 'io.grpc:protoc-gen-grpc-java:1.75.0-SNAPSHOT' // CURRENT_GRPC_VERSION grpc { artifact = 'io.grpc:protoc-gen-grpc-java:1.76.0-SNAPSHOT' // CURRENT_GRPC_VERSION
} }
} }
generateProtoTasks { generateProtoTasks {
@ -54,12 +54,12 @@ dependencies {
implementation 'androidx.appcompat:appcompat:1.0.0' implementation 'androidx.appcompat:appcompat:1.0.0'
// You need to build grpc-java to obtain these libraries below. // You need to build grpc-java to obtain these libraries below.
implementation 'io.grpc:grpc-okhttp:1.75.0-SNAPSHOT' // CURRENT_GRPC_VERSION implementation 'io.grpc:grpc-okhttp:1.76.0-SNAPSHOT' // CURRENT_GRPC_VERSION
implementation 'io.grpc:grpc-protobuf-lite:1.75.0-SNAPSHOT' // CURRENT_GRPC_VERSION implementation 'io.grpc:grpc-protobuf-lite:1.76.0-SNAPSHOT' // CURRENT_GRPC_VERSION
implementation 'io.grpc:grpc-stub:1.75.0-SNAPSHOT' // CURRENT_GRPC_VERSION implementation 'io.grpc:grpc-stub:1.76.0-SNAPSHOT' // CURRENT_GRPC_VERSION
implementation 'org.apache.tomcat:annotations-api:6.0.53' implementation 'org.apache.tomcat:annotations-api:6.0.53'
testImplementation 'junit:junit:4.13.2' testImplementation 'junit:junit:4.13.2'
testImplementation 'com.google.truth:truth:1.1.5' testImplementation 'com.google.truth:truth:1.1.5'
testImplementation 'io.grpc:grpc-testing:1.75.0-SNAPSHOT' // CURRENT_GRPC_VERSION testImplementation 'io.grpc:grpc-testing:1.76.0-SNAPSHOT' // CURRENT_GRPC_VERSION
} }

View File

@ -32,7 +32,7 @@ android {
protobuf { protobuf {
protoc { artifact = 'com.google.protobuf:protoc:3.25.1' } protoc { artifact = 'com.google.protobuf:protoc:3.25.1' }
plugins { plugins {
grpc { artifact = 'io.grpc:protoc-gen-grpc-java:1.75.0-SNAPSHOT' // CURRENT_GRPC_VERSION grpc { artifact = 'io.grpc:protoc-gen-grpc-java:1.76.0-SNAPSHOT' // CURRENT_GRPC_VERSION
} }
} }
generateProtoTasks { generateProtoTasks {
@ -52,8 +52,8 @@ dependencies {
implementation 'androidx.appcompat:appcompat:1.0.0' implementation 'androidx.appcompat:appcompat:1.0.0'
// You need to build grpc-java to obtain these libraries below. // You need to build grpc-java to obtain these libraries below.
implementation 'io.grpc:grpc-okhttp:1.75.0-SNAPSHOT' // CURRENT_GRPC_VERSION implementation 'io.grpc:grpc-okhttp:1.76.0-SNAPSHOT' // CURRENT_GRPC_VERSION
implementation 'io.grpc:grpc-protobuf-lite:1.75.0-SNAPSHOT' // CURRENT_GRPC_VERSION implementation 'io.grpc:grpc-protobuf-lite:1.76.0-SNAPSHOT' // CURRENT_GRPC_VERSION
implementation 'io.grpc:grpc-stub:1.75.0-SNAPSHOT' // CURRENT_GRPC_VERSION implementation 'io.grpc:grpc-stub:1.76.0-SNAPSHOT' // CURRENT_GRPC_VERSION
implementation 'org.apache.tomcat:annotations-api:6.0.53' implementation 'org.apache.tomcat:annotations-api:6.0.53'
} }

View File

@ -32,7 +32,7 @@ android {
protobuf { protobuf {
protoc { artifact = 'com.google.protobuf:protoc:3.25.1' } protoc { artifact = 'com.google.protobuf:protoc:3.25.1' }
plugins { plugins {
grpc { artifact = 'io.grpc:protoc-gen-grpc-java:1.75.0-SNAPSHOT' // CURRENT_GRPC_VERSION grpc { artifact = 'io.grpc:protoc-gen-grpc-java:1.76.0-SNAPSHOT' // CURRENT_GRPC_VERSION
} }
} }
generateProtoTasks { generateProtoTasks {
@ -52,8 +52,8 @@ dependencies {
implementation 'androidx.appcompat:appcompat:1.0.0' implementation 'androidx.appcompat:appcompat:1.0.0'
// You need to build grpc-java to obtain these libraries below. // You need to build grpc-java to obtain these libraries below.
implementation 'io.grpc:grpc-okhttp:1.75.0-SNAPSHOT' // CURRENT_GRPC_VERSION implementation 'io.grpc:grpc-okhttp:1.76.0-SNAPSHOT' // CURRENT_GRPC_VERSION
implementation 'io.grpc:grpc-protobuf-lite:1.75.0-SNAPSHOT' // CURRENT_GRPC_VERSION implementation 'io.grpc:grpc-protobuf-lite:1.76.0-SNAPSHOT' // CURRENT_GRPC_VERSION
implementation 'io.grpc:grpc-stub:1.75.0-SNAPSHOT' // CURRENT_GRPC_VERSION implementation 'io.grpc:grpc-stub:1.76.0-SNAPSHOT' // CURRENT_GRPC_VERSION
implementation 'org.apache.tomcat:annotations-api:6.0.53' implementation 'org.apache.tomcat:annotations-api:6.0.53'
} }

View File

@ -33,7 +33,7 @@ android {
protobuf { protobuf {
protoc { artifact = 'com.google.protobuf:protoc:3.25.1' } protoc { artifact = 'com.google.protobuf:protoc:3.25.1' }
plugins { plugins {
grpc { artifact = 'io.grpc:protoc-gen-grpc-java:1.75.0-SNAPSHOT' // CURRENT_GRPC_VERSION grpc { artifact = 'io.grpc:protoc-gen-grpc-java:1.76.0-SNAPSHOT' // CURRENT_GRPC_VERSION
} }
} }
generateProtoTasks { generateProtoTasks {
@ -53,8 +53,8 @@ dependencies {
implementation 'androidx.appcompat:appcompat:1.0.0' implementation 'androidx.appcompat:appcompat:1.0.0'
// You need to build grpc-java to obtain these libraries below. // You need to build grpc-java to obtain these libraries below.
implementation 'io.grpc:grpc-okhttp:1.75.0-SNAPSHOT' // CURRENT_GRPC_VERSION implementation 'io.grpc:grpc-okhttp:1.76.0-SNAPSHOT' // CURRENT_GRPC_VERSION
implementation 'io.grpc:grpc-protobuf-lite:1.75.0-SNAPSHOT' // CURRENT_GRPC_VERSION implementation 'io.grpc:grpc-protobuf-lite:1.76.0-SNAPSHOT' // CURRENT_GRPC_VERSION
implementation 'io.grpc:grpc-stub:1.75.0-SNAPSHOT' // CURRENT_GRPC_VERSION implementation 'io.grpc:grpc-stub:1.76.0-SNAPSHOT' // CURRENT_GRPC_VERSION
implementation 'org.apache.tomcat:annotations-api:6.0.53' implementation 'org.apache.tomcat:annotations-api:6.0.53'
} }

View File

@ -21,8 +21,8 @@ java {
// Feel free to delete the comment at the next line. It is just for safely // Feel free to delete the comment at the next line. It is just for safely
// updating the version in our release process. // updating the version in our release process.
def grpcVersion = '1.75.0-SNAPSHOT' // CURRENT_GRPC_VERSION def grpcVersion = '1.76.0-SNAPSHOT' // CURRENT_GRPC_VERSION
def protobufVersion = '3.25.5' def protobufVersion = '3.25.8'
def protocVersion = protobufVersion def protocVersion = protobufVersion
dependencies { dependencies {

View File

@ -21,8 +21,8 @@ java {
// Feel free to delete the comment at the next line. It is just for safely // Feel free to delete the comment at the next line. It is just for safely
// updating the version in our release process. // updating the version in our release process.
def grpcVersion = '1.75.0-SNAPSHOT' // CURRENT_GRPC_VERSION def grpcVersion = '1.76.0-SNAPSHOT' // CURRENT_GRPC_VERSION
def protocVersion = '3.25.5' def protocVersion = '3.25.8'
dependencies { dependencies {
// grpc-alts transitively depends on grpc-netty-shaded, grpc-protobuf, and grpc-stub // grpc-alts transitively depends on grpc-netty-shaded, grpc-protobuf, and grpc-stub

View File

@ -23,8 +23,8 @@ java {
// Feel free to delete the comment at the next line. It is just for safely // Feel free to delete the comment at the next line. It is just for safely
// updating the version in our release process. // updating the version in our release process.
def grpcVersion = '1.75.0-SNAPSHOT' // CURRENT_GRPC_VERSION def grpcVersion = '1.76.0-SNAPSHOT' // CURRENT_GRPC_VERSION
def protobufVersion = '3.25.5' def protobufVersion = '3.25.8'
dependencies { dependencies {
implementation "io.grpc:grpc-protobuf:${grpcVersion}" implementation "io.grpc:grpc-protobuf:${grpcVersion}"

View File

@ -6,14 +6,14 @@
<packaging>jar</packaging> <packaging>jar</packaging>
<!-- Feel free to delete the comment at the end of these lines. It is just <!-- Feel free to delete the comment at the end of these lines. It is just
for safely updating the version in our release process. --> for safely updating the version in our release process. -->
<version>1.75.0-SNAPSHOT</version><!-- CURRENT_GRPC_VERSION --> <version>1.76.0-SNAPSHOT</version><!-- CURRENT_GRPC_VERSION -->
<name>example-debug</name> <name>example-debug</name>
<url>https://github.com/grpc/grpc-java</url> <url>https://github.com/grpc/grpc-java</url>
<properties> <properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<grpc.version>1.75.0-SNAPSHOT</grpc.version><!-- CURRENT_GRPC_VERSION --> <grpc.version>1.76.0-SNAPSHOT</grpc.version><!-- CURRENT_GRPC_VERSION -->
<protoc.version>3.25.5</protoc.version> <protoc.version>3.25.8</protoc.version>
<!-- required for jdk9 --> <!-- required for jdk9 -->
<maven.compiler.source>1.8</maven.compiler.source> <maven.compiler.source>1.8</maven.compiler.source>
<maven.compiler.target>1.8</maven.compiler.target> <maven.compiler.target>1.8</maven.compiler.target>

View File

@ -23,8 +23,8 @@ java {
// Feel free to delete the comment at the next line. It is just for safely // Feel free to delete the comment at the next line. It is just for safely
// updating the version in our release process. // updating the version in our release process.
def grpcVersion = '1.75.0-SNAPSHOT' // CURRENT_GRPC_VERSION def grpcVersion = '1.76.0-SNAPSHOT' // CURRENT_GRPC_VERSION
def protobufVersion = '3.25.5' def protobufVersion = '3.25.8'
dependencies { dependencies {
implementation "io.grpc:grpc-protobuf:${grpcVersion}" implementation "io.grpc:grpc-protobuf:${grpcVersion}"

View File

@ -6,14 +6,14 @@
<packaging>jar</packaging> <packaging>jar</packaging>
<!-- Feel free to delete the comment at the end of these lines. It is just <!-- Feel free to delete the comment at the end of these lines. It is just
for safely updating the version in our release process. --> for safely updating the version in our release process. -->
<version>1.75.0-SNAPSHOT</version><!-- CURRENT_GRPC_VERSION --> <version>1.76.0-SNAPSHOT</version><!-- CURRENT_GRPC_VERSION -->
<name>example-dualstack</name> <name>example-dualstack</name>
<url>https://github.com/grpc/grpc-java</url> <url>https://github.com/grpc/grpc-java</url>
<properties> <properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<grpc.version>1.75.0-SNAPSHOT</grpc.version><!-- CURRENT_GRPC_VERSION --> <grpc.version>1.76.0-SNAPSHOT</grpc.version><!-- CURRENT_GRPC_VERSION -->
<protoc.version>3.25.5</protoc.version> <protoc.version>3.25.8</protoc.version>
<!-- required for jdk9 --> <!-- required for jdk9 -->
<maven.compiler.source>1.8</maven.compiler.source> <maven.compiler.source>1.8</maven.compiler.source>
<maven.compiler.target>1.8</maven.compiler.target> <maven.compiler.target>1.8</maven.compiler.target>

View File

@ -21,8 +21,8 @@ java {
// Feel free to delete the comment at the next line. It is just for safely // Feel free to delete the comment at the next line. It is just for safely
// updating the version in our release process. // updating the version in our release process.
def grpcVersion = '1.75.0-SNAPSHOT' // CURRENT_GRPC_VERSION def grpcVersion = '1.76.0-SNAPSHOT' // CURRENT_GRPC_VERSION
def protobufVersion = '3.25.5' def protobufVersion = '3.25.8'
def protocVersion = protobufVersion def protocVersion = protobufVersion

View File

@ -6,14 +6,14 @@
<packaging>jar</packaging> <packaging>jar</packaging>
<!-- Feel free to delete the comment at the end of these lines. It is just <!-- Feel free to delete the comment at the end of these lines. It is just
for safely updating the version in our release process. --> for safely updating the version in our release process. -->
<version>1.75.0-SNAPSHOT</version><!-- CURRENT_GRPC_VERSION --> <version>1.76.0-SNAPSHOT</version><!-- CURRENT_GRPC_VERSION -->
<name>example-gauth</name> <name>example-gauth</name>
<url>https://github.com/grpc/grpc-java</url> <url>https://github.com/grpc/grpc-java</url>
<properties> <properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<grpc.version>1.75.0-SNAPSHOT</grpc.version><!-- CURRENT_GRPC_VERSION --> <grpc.version>1.76.0-SNAPSHOT</grpc.version><!-- CURRENT_GRPC_VERSION -->
<protobuf.version>3.25.5</protobuf.version> <protobuf.version>3.25.8</protobuf.version>
<!-- required for jdk9 --> <!-- required for jdk9 -->
<maven.compiler.source>1.8</maven.compiler.source> <maven.compiler.source>1.8</maven.compiler.source>
<maven.compiler.target>1.8</maven.compiler.target> <maven.compiler.target>1.8</maven.compiler.target>

View File

@ -22,10 +22,10 @@ java {
// Feel free to delete the comment at the next line. It is just for safely // Feel free to delete the comment at the next line. It is just for safely
// updating the version in our release process. // updating the version in our release process.
def grpcVersion = '1.75.0-SNAPSHOT' // CURRENT_GRPC_VERSION def grpcVersion = '1.76.0-SNAPSHOT' // CURRENT_GRPC_VERSION
def protocVersion = '3.25.5' def protocVersion = '3.25.8'
def openTelemetryVersion = '1.40.0' def openTelemetryVersion = '1.52.0'
def openTelemetryPrometheusVersion = '1.40.0-alpha' def openTelemetryPrometheusVersion = '1.52.0-alpha'
dependencies { dependencies {
implementation "io.grpc:grpc-protobuf:${grpcVersion}" implementation "io.grpc:grpc-protobuf:${grpcVersion}"

View File

@ -22,8 +22,8 @@ java {
// Feel free to delete the comment at the next line. It is just for safely // Feel free to delete the comment at the next line. It is just for safely
// updating the version in our release process. // updating the version in our release process.
def grpcVersion = '1.75.0-SNAPSHOT' // CURRENT_GRPC_VERSION def grpcVersion = '1.76.0-SNAPSHOT' // CURRENT_GRPC_VERSION
def protocVersion = '3.25.5' def protocVersion = '3.25.8'
dependencies { dependencies {
implementation "io.grpc:grpc-protobuf:${grpcVersion}" implementation "io.grpc:grpc-protobuf:${grpcVersion}"

View File

@ -21,8 +21,8 @@ java {
// Feel free to delete the comment at the next line. It is just for safely // Feel free to delete the comment at the next line. It is just for safely
// updating the version in our release process. // updating the version in our release process.
def grpcVersion = '1.75.0-SNAPSHOT' // CURRENT_GRPC_VERSION def grpcVersion = '1.76.0-SNAPSHOT' // CURRENT_GRPC_VERSION
def protobufVersion = '3.25.5' def protobufVersion = '3.25.8'
dependencies { dependencies {
implementation "io.grpc:grpc-protobuf:${grpcVersion}" implementation "io.grpc:grpc-protobuf:${grpcVersion}"

View File

@ -6,14 +6,14 @@
<packaging>jar</packaging> <packaging>jar</packaging>
<!-- Feel free to delete the comment at the end of these lines. It is just <!-- Feel free to delete the comment at the end of these lines. It is just
for safely updating the version in our release process. --> for safely updating the version in our release process. -->
<version>1.75.0-SNAPSHOT</version><!-- CURRENT_GRPC_VERSION --> <version>1.76.0-SNAPSHOT</version><!-- CURRENT_GRPC_VERSION -->
<name>example-hostname</name> <name>example-hostname</name>
<url>https://github.com/grpc/grpc-java</url> <url>https://github.com/grpc/grpc-java</url>
<properties> <properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<grpc.version>1.75.0-SNAPSHOT</grpc.version><!-- CURRENT_GRPC_VERSION --> <grpc.version>1.76.0-SNAPSHOT</grpc.version><!-- CURRENT_GRPC_VERSION -->
<protoc.version>3.25.5</protoc.version> <protoc.version>3.25.8</protoc.version>
<!-- required for jdk9 --> <!-- required for jdk9 -->
<maven.compiler.source>1.8</maven.compiler.source> <maven.compiler.source>1.8</maven.compiler.source>
<maven.compiler.target>1.8</maven.compiler.target> <maven.compiler.target>1.8</maven.compiler.target>

View File

@ -21,8 +21,8 @@ java {
// Feel free to delete the comment at the next line. It is just for safely // Feel free to delete the comment at the next line. It is just for safely
// updating the version in our release process. // updating the version in our release process.
def grpcVersion = '1.75.0-SNAPSHOT' // CURRENT_GRPC_VERSION def grpcVersion = '1.76.0-SNAPSHOT' // CURRENT_GRPC_VERSION
def protobufVersion = '3.25.5' def protobufVersion = '3.25.8'
def protocVersion = protobufVersion def protocVersion = protobufVersion
dependencies { dependencies {

View File

@ -7,15 +7,15 @@
<packaging>jar</packaging> <packaging>jar</packaging>
<!-- Feel free to delete the comment at the end of these lines. It is just <!-- Feel free to delete the comment at the end of these lines. It is just
for safely updating the version in our release process. --> for safely updating the version in our release process. -->
<version>1.75.0-SNAPSHOT</version><!-- CURRENT_GRPC_VERSION --> <version>1.76.0-SNAPSHOT</version><!-- CURRENT_GRPC_VERSION -->
<name>example-jwt-auth</name> <name>example-jwt-auth</name>
<url>https://github.com/grpc/grpc-java</url> <url>https://github.com/grpc/grpc-java</url>
<properties> <properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<grpc.version>1.75.0-SNAPSHOT</grpc.version><!-- CURRENT_GRPC_VERSION --> <grpc.version>1.76.0-SNAPSHOT</grpc.version><!-- CURRENT_GRPC_VERSION -->
<protobuf.version>3.25.5</protobuf.version> <protobuf.version>3.25.8</protobuf.version>
<protoc.version>3.25.5</protoc.version> <protoc.version>3.25.8</protoc.version>
<!-- required for jdk9 --> <!-- required for jdk9 -->
<maven.compiler.source>1.8</maven.compiler.source> <maven.compiler.source>1.8</maven.compiler.source>
<maven.compiler.target>1.8</maven.compiler.target> <maven.compiler.target>1.8</maven.compiler.target>

View File

@ -21,8 +21,8 @@ java {
// Feel free to delete the comment at the next line. It is just for safely // Feel free to delete the comment at the next line. It is just for safely
// updating the version in our release process. // updating the version in our release process.
def grpcVersion = '1.75.0-SNAPSHOT' // CURRENT_GRPC_VERSION def grpcVersion = '1.76.0-SNAPSHOT' // CURRENT_GRPC_VERSION
def protobufVersion = '3.25.5' def protobufVersion = '3.25.8'
def protocVersion = protobufVersion def protocVersion = protobufVersion
dependencies { dependencies {

View File

@ -7,15 +7,15 @@
<packaging>jar</packaging> <packaging>jar</packaging>
<!-- Feel free to delete the comment at the end of these lines. It is just <!-- Feel free to delete the comment at the end of these lines. It is just
for safely updating the version in our release process. --> for safely updating the version in our release process. -->
<version>1.75.0-SNAPSHOT</version><!-- CURRENT_GRPC_VERSION --> <version>1.76.0-SNAPSHOT</version><!-- CURRENT_GRPC_VERSION -->
<name>example-oauth</name> <name>example-oauth</name>
<url>https://github.com/grpc/grpc-java</url> <url>https://github.com/grpc/grpc-java</url>
<properties> <properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<grpc.version>1.75.0-SNAPSHOT</grpc.version><!-- CURRENT_GRPC_VERSION --> <grpc.version>1.76.0-SNAPSHOT</grpc.version><!-- CURRENT_GRPC_VERSION -->
<protobuf.version>3.25.5</protobuf.version> <protobuf.version>3.25.8</protobuf.version>
<protoc.version>3.25.5</protoc.version> <protoc.version>3.25.8</protoc.version>
<!-- required for jdk9 --> <!-- required for jdk9 -->
<maven.compiler.source>1.8</maven.compiler.source> <maven.compiler.source>1.8</maven.compiler.source>
<maven.compiler.target>1.8</maven.compiler.target> <maven.compiler.target>1.8</maven.compiler.target>

View File

@ -21,10 +21,10 @@ java {
// Feel free to delete the comment at the next line. It is just for safely // Feel free to delete the comment at the next line. It is just for safely
// updating the version in our release process. // updating the version in our release process.
def grpcVersion = '1.75.0-SNAPSHOT' // CURRENT_GRPC_VERSION def grpcVersion = '1.76.0-SNAPSHOT' // CURRENT_GRPC_VERSION
def protocVersion = '3.25.5' def protocVersion = '3.25.8'
def openTelemetryVersion = '1.40.0' def openTelemetryVersion = '1.52.0'
def openTelemetryPrometheusVersion = '1.40.0-alpha' def openTelemetryPrometheusVersion = '1.52.0-alpha'
dependencies { dependencies {
implementation "io.grpc:grpc-protobuf:${grpcVersion}" implementation "io.grpc:grpc-protobuf:${grpcVersion}"

View File

@ -16,8 +16,8 @@ java {
targetCompatibility = JavaVersion.VERSION_1_8 targetCompatibility = JavaVersion.VERSION_1_8
} }
def grpcVersion = '1.75.0-SNAPSHOT' // CURRENT_GRPC_VERSION def grpcVersion = '1.76.0-SNAPSHOT' // CURRENT_GRPC_VERSION
def protocVersion = '3.25.5' def protocVersion = '3.25.8'
dependencies { dependencies {
implementation "io.grpc:grpc-protobuf:${grpcVersion}" implementation "io.grpc:grpc-protobuf:${grpcVersion}"

View File

@ -16,8 +16,8 @@ java {
targetCompatibility = JavaVersion.VERSION_1_8 targetCompatibility = JavaVersion.VERSION_1_8
} }
def grpcVersion = '1.75.0-SNAPSHOT' // CURRENT_GRPC_VERSION def grpcVersion = '1.76.0-SNAPSHOT' // CURRENT_GRPC_VERSION
def protocVersion = '3.25.5' def protocVersion = '3.25.8'
dependencies { dependencies {
implementation "io.grpc:grpc-protobuf:${grpcVersion}" implementation "io.grpc:grpc-protobuf:${grpcVersion}"

View File

@ -15,8 +15,8 @@ java {
targetCompatibility = JavaVersion.VERSION_1_8 targetCompatibility = JavaVersion.VERSION_1_8
} }
def grpcVersion = '1.75.0-SNAPSHOT' // CURRENT_GRPC_VERSION def grpcVersion = '1.76.0-SNAPSHOT' // CURRENT_GRPC_VERSION
def protocVersion = '3.25.5' def protocVersion = '3.25.8'
dependencies { dependencies {
implementation "io.grpc:grpc-protobuf:${grpcVersion}", implementation "io.grpc:grpc-protobuf:${grpcVersion}",

View File

@ -21,8 +21,8 @@ java {
// Feel free to delete the comment at the next line. It is just for safely // Feel free to delete the comment at the next line. It is just for safely
// updating the version in our release process. // updating the version in our release process.
def grpcVersion = '1.75.0-SNAPSHOT' // CURRENT_GRPC_VERSION def grpcVersion = '1.76.0-SNAPSHOT' // CURRENT_GRPC_VERSION
def protocVersion = '3.25.5' def protocVersion = '3.25.8'
dependencies { dependencies {
implementation "io.grpc:grpc-protobuf:${grpcVersion}" implementation "io.grpc:grpc-protobuf:${grpcVersion}"

View File

@ -6,14 +6,14 @@
<packaging>jar</packaging> <packaging>jar</packaging>
<!-- Feel free to delete the comment at the end of these lines. It is just <!-- Feel free to delete the comment at the end of these lines. It is just
for safely updating the version in our release process. --> for safely updating the version in our release process. -->
<version>1.75.0-SNAPSHOT</version><!-- CURRENT_GRPC_VERSION --> <version>1.76.0-SNAPSHOT</version><!-- CURRENT_GRPC_VERSION -->
<name>example-tls</name> <name>example-tls</name>
<url>https://github.com/grpc/grpc-java</url> <url>https://github.com/grpc/grpc-java</url>
<properties> <properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<grpc.version>1.75.0-SNAPSHOT</grpc.version><!-- CURRENT_GRPC_VERSION --> <grpc.version>1.76.0-SNAPSHOT</grpc.version><!-- CURRENT_GRPC_VERSION -->
<protoc.version>3.25.5</protoc.version> <protoc.version>3.25.8</protoc.version>
<!-- required for jdk9 --> <!-- required for jdk9 -->
<maven.compiler.source>1.8</maven.compiler.source> <maven.compiler.source>1.8</maven.compiler.source>
<maven.compiler.target>1.8</maven.compiler.target> <maven.compiler.target>1.8</maven.compiler.target>

View File

@ -21,8 +21,8 @@ java {
// Feel free to delete the comment at the next line. It is just for safely // Feel free to delete the comment at the next line. It is just for safely
// updating the version in our release process. // updating the version in our release process.
def grpcVersion = '1.75.0-SNAPSHOT' // CURRENT_GRPC_VERSION def grpcVersion = '1.76.0-SNAPSHOT' // CURRENT_GRPC_VERSION
def protocVersion = '3.25.5' def protocVersion = '3.25.8'
dependencies { dependencies {
implementation "io.grpc:grpc-protobuf:${grpcVersion}" implementation "io.grpc:grpc-protobuf:${grpcVersion}"

View File

@ -6,15 +6,15 @@
<packaging>jar</packaging> <packaging>jar</packaging>
<!-- Feel free to delete the comment at the end of these lines. It is just <!-- Feel free to delete the comment at the end of these lines. It is just
for safely updating the version in our release process. --> for safely updating the version in our release process. -->
<version>1.75.0-SNAPSHOT</version><!-- CURRENT_GRPC_VERSION --> <version>1.76.0-SNAPSHOT</version><!-- CURRENT_GRPC_VERSION -->
<name>examples</name> <name>examples</name>
<url>https://github.com/grpc/grpc-java</url> <url>https://github.com/grpc/grpc-java</url>
<properties> <properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<grpc.version>1.75.0-SNAPSHOT</grpc.version><!-- CURRENT_GRPC_VERSION --> <grpc.version>1.76.0-SNAPSHOT</grpc.version><!-- CURRENT_GRPC_VERSION -->
<protobuf.version>3.25.5</protobuf.version> <protobuf.version>3.25.8</protobuf.version>
<protoc.version>3.25.5</protoc.version> <protoc.version>3.25.8</protoc.version>
<!-- required for JDK 8 --> <!-- required for JDK 8 -->
<maven.compiler.source>1.8</maven.compiler.source> <maven.compiler.source>1.8</maven.compiler.source>
<maven.compiler.target>1.8</maven.compiler.target> <maven.compiler.target>1.8</maven.compiler.target>

View File

@ -1,19 +1,21 @@
[versions] [versions]
netty = '4.1.110.Final' netty = '4.1.124.Final'
# Keep the following references of tcnative version in sync whenever it's updated: # Keep the following references of tcnative version in sync whenever it's updated:
# SECURITY.md # SECURITY.md
nettytcnative = '2.0.70.Final' nettytcnative = '2.0.72.Final'
opencensus = "0.31.1" opencensus = "0.31.1"
# Not upgrading to 4.x as it is not yet ABI compatible. # Not upgrading to 4.x as it is not yet ABI compatible.
# https://github.com/protocolbuffers/protobuf/issues/17247 # https://github.com/protocolbuffers/protobuf/issues/17247
protobuf = "3.25.5" protobuf = "3.25.8"
[libraries] [libraries]
android-annotations = "com.google.android:annotations:4.1.1.4" android-annotations = "com.google.android:annotations:4.1.1.4"
# androidx-annotation 1.9.1+ uses Kotlin and requires Android Gradle Plugin 9+
androidx-annotation = "androidx.annotation:annotation:1.9.0" androidx-annotation = "androidx.annotation:annotation:1.9.0"
# 1.15.0 requires libraries and applications that depend on it to compile against # 1.15.0 requires libraries and applications that depend on it to compile against
# version 35 or later of the Android APIs. # version 35 or later of the Android APIs.
androidx-core = "androidx.core:core:1.13.1" androidx-core = "androidx.core:core:1.13.1"
# androidx-lifecycle 2.9+ requires Java 17
androidx-lifecycle-common = "androidx.lifecycle:lifecycle-common:2.8.7" androidx-lifecycle-common = "androidx.lifecycle:lifecycle-common:2.8.7"
androidx-lifecycle-service = "androidx.lifecycle:lifecycle-service:2.8.7" androidx-lifecycle-service = "androidx.lifecycle:lifecycle-service:2.8.7"
androidx-test-core = "androidx.test:core:1.6.1" androidx-test-core = "androidx.test:core:1.6.1"
@ -36,13 +38,13 @@ cronet-embedded = "org.chromium.net:cronet-embedded:119.6045.31"
errorprone-annotations = "com.google.errorprone:error_prone_annotations:2.30.0" errorprone-annotations = "com.google.errorprone:error_prone_annotations:2.30.0"
# error-prone 2.32.0+ require Java 17+ # error-prone 2.32.0+ require Java 17+
errorprone-core = "com.google.errorprone:error_prone_core:2.31.0" errorprone-core = "com.google.errorprone:error_prone_core:2.31.0"
google-api-protos = "com.google.api.grpc:proto-google-common-protos:2.51.0" google-api-protos = "com.google.api.grpc:proto-google-common-protos:2.59.2"
# google-auth-library 1.25.0+ requires error_prone_annotations 2.31.0+, which # google-auth-library 1.25.0+ requires error_prone_annotations 2.31.0+, which
# breaks the Android build # breaks the Android build
google-auth-credentials = "com.google.auth:google-auth-library-credentials:1.24.1" google-auth-credentials = "com.google.auth:google-auth-library-credentials:1.24.1"
google-auth-oauth2Http = "com.google.auth:google-auth-library-oauth2-http:1.24.1" google-auth-oauth2Http = "com.google.auth:google-auth-library-oauth2-http:1.24.1"
# Release notes: https://cloud.google.com/logging/docs/release-notes # Release notes: https://cloud.google.com/logging/docs/release-notes
google-cloud-logging = "com.google.cloud:google-cloud-logging:3.21.2" google-cloud-logging = "com.google.cloud:google-cloud-logging:3.23.1"
# 2.12.1 requires error_prone_annotations:2.36.0 but we are stuck with 2.30.0 # 2.12.1 requires error_prone_annotations:2.36.0 but we are stuck with 2.30.0
gson = "com.google.code.gson:gson:2.11.0" gson = "com.google.code.gson:gson:2.11.0"
# 33.4.0 requires com.google.errorprone:error_prone_annotations:2.36.0 but we are stuck with 2.30.0 (see above) # 33.4.0 requires com.google.errorprone:error_prone_annotations:2.36.0 but we are stuck with 2.30.0 (see above)
@ -61,7 +63,7 @@ javax-annotation = "org.apache.tomcat:annotations-api:6.0.53"
javax-servlet-api = "javax.servlet:javax.servlet-api:4.0.1" javax-servlet-api = "javax.servlet:javax.servlet-api:4.0.1"
# 12.0.0+ require Java 17+ # 12.0.0+ require Java 17+
jetty-client = "org.eclipse.jetty:jetty-client:11.0.24" jetty-client = "org.eclipse.jetty:jetty-client:11.0.24"
jetty-http2-server = "org.eclipse.jetty.http2:jetty-http2-server:12.0.16" jetty-http2-server = "org.eclipse.jetty.http2:jetty-http2-server:12.0.23"
jetty-http2-server10 = "org.eclipse.jetty.http2:http2-server:10.0.20" jetty-http2-server10 = "org.eclipse.jetty.http2:http2-server:10.0.20"
jetty-servlet = "org.eclipse.jetty.ee10:jetty-ee10-servlet:12.0.16" jetty-servlet = "org.eclipse.jetty.ee10:jetty-ee10-servlet:12.0.16"
jetty-servlet10 = "org.eclipse.jetty:jetty-servlet:10.0.20" jetty-servlet10 = "org.eclipse.jetty:jetty-servlet:10.0.20"
@ -91,19 +93,19 @@ opencensus-contrib-grpc-metrics = { module = "io.opencensus:opencensus-contrib-g
opencensus-exporter-stats-stackdriver = { module = "io.opencensus:opencensus-exporter-stats-stackdriver", version.ref = "opencensus" } opencensus-exporter-stats-stackdriver = { module = "io.opencensus:opencensus-exporter-stats-stackdriver", version.ref = "opencensus" }
opencensus-exporter-trace-stackdriver = { module = "io.opencensus:opencensus-exporter-trace-stackdriver", version.ref = "opencensus" } opencensus-exporter-trace-stackdriver = { module = "io.opencensus:opencensus-exporter-trace-stackdriver", version.ref = "opencensus" }
opencensus-impl = { module = "io.opencensus:opencensus-impl", version.ref = "opencensus" } opencensus-impl = { module = "io.opencensus:opencensus-impl", version.ref = "opencensus" }
opentelemetry-api = "io.opentelemetry:opentelemetry-api:1.46.0" opentelemetry-api = "io.opentelemetry:opentelemetry-api:1.52.0"
opentelemetry-exporter-prometheus = "io.opentelemetry:opentelemetry-exporter-prometheus:1.46.0-alpha" opentelemetry-exporter-prometheus = "io.opentelemetry:opentelemetry-exporter-prometheus:1.52.0-alpha"
opentelemetry-gcp-resources = "io.opentelemetry.contrib:opentelemetry-gcp-resources:1.43.0-alpha" opentelemetry-gcp-resources = "io.opentelemetry.contrib:opentelemetry-gcp-resources:1.48.0-alpha"
opentelemetry-sdk-extension-autoconfigure = "io.opentelemetry:opentelemetry-sdk-extension-autoconfigure:1.46.0" opentelemetry-sdk-extension-autoconfigure = "io.opentelemetry:opentelemetry-sdk-extension-autoconfigure:1.52.0"
opentelemetry-sdk-testing = "io.opentelemetry:opentelemetry-sdk-testing:1.46.0" opentelemetry-sdk-testing = "io.opentelemetry:opentelemetry-sdk-testing:1.52.0"
perfmark-api = "io.perfmark:perfmark-api:0.27.0" perfmark-api = "io.perfmark:perfmark-api:0.27.0"
protobuf-java = { module = "com.google.protobuf:protobuf-java", version.ref = "protobuf" } protobuf-java = { module = "com.google.protobuf:protobuf-java", version.ref = "protobuf" }
protobuf-java-util = { module = "com.google.protobuf:protobuf-java-util", version.ref = "protobuf" } protobuf-java-util = { module = "com.google.protobuf:protobuf-java-util", version.ref = "protobuf" }
protobuf-javalite = { module = "com.google.protobuf:protobuf-javalite", version.ref = "protobuf" } protobuf-javalite = { module = "com.google.protobuf:protobuf-javalite", version.ref = "protobuf" }
protobuf-protoc = { module = "com.google.protobuf:protoc", version.ref = "protobuf" } protobuf-protoc = { module = "com.google.protobuf:protoc", version.ref = "protobuf" }
re2j = "com.google.re2j:re2j:1.8" re2j = "com.google.re2j:re2j:1.8"
robolectric = "org.robolectric:robolectric:4.14.1" robolectric = "org.robolectric:robolectric:4.15.1"
s2a-proto = "com.google.s2a.proto.v2:s2a-proto:0.1.1" s2a-proto = "com.google.s2a.proto.v2:s2a-proto:0.1.2"
signature-android = "net.sf.androidscents.signature:android-api-level-21:5.0.1_r2" signature-android = "net.sf.androidscents.signature:android-api-level-21:5.0.1_r2"
signature-java = "org.codehaus.mojo.signature:java18:1.0" signature-java = "org.codehaus.mojo.signature:java18:1.0"
# 11.0.0+ require Java 17+ # 11.0.0+ require Java 17+

8
lint.xml Normal file
View File

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<lint>
<!--
Suppress temporarily due to AAPT2 failures with SDK 35/36 on AGP 7.x.
Remove after AGP upgrade.
-->
<issue id="OldTargetApi" severity="ignore" />
</lint>

View File

@ -84,6 +84,7 @@ public class GrpcSslContexts {
private static final String SUN_PROVIDER_NAME = "SunJSSE"; private static final String SUN_PROVIDER_NAME = "SunJSSE";
private static final String IBM_PROVIDER_NAME = "IBMJSSE2"; private static final String IBM_PROVIDER_NAME = "IBMJSSE2";
private static final String OPENJSSE_PROVIDER_NAME = "OpenJSSE"; private static final String OPENJSSE_PROVIDER_NAME = "OpenJSSE";
private static final String BCJSSE_PROVIDER_NAME = "BCJSSE";
/** /**
* Creates an SslContextBuilder with ciphers and APN appropriate for gRPC. * Creates an SslContextBuilder with ciphers and APN appropriate for gRPC.
@ -199,7 +200,8 @@ public class GrpcSslContexts {
jdkProvider.getName() + " selected, but Java 9+ and Jetty NPN/ALPN unavailable"); jdkProvider.getName() + " selected, but Java 9+ and Jetty NPN/ALPN unavailable");
} }
} else if (IBM_PROVIDER_NAME.equals(jdkProvider.getName()) } else if (IBM_PROVIDER_NAME.equals(jdkProvider.getName())
|| OPENJSSE_PROVIDER_NAME.equals(jdkProvider.getName())) { || OPENJSSE_PROVIDER_NAME.equals(jdkProvider.getName())
|| BCJSSE_PROVIDER_NAME.equals(jdkProvider.getName())) {
if (JettyTlsUtil.isJava9AlpnAvailable()) { if (JettyTlsUtil.isJava9AlpnAvailable()) {
apc = ALPN; apc = ALPN;
} else { } else {
@ -255,7 +257,8 @@ public class GrpcSslContexts {
return provider; return provider;
} }
} else if (IBM_PROVIDER_NAME.equals(provider.getName()) } else if (IBM_PROVIDER_NAME.equals(provider.getName())
|| OPENJSSE_PROVIDER_NAME.equals(provider.getName())) { || OPENJSSE_PROVIDER_NAME.equals(provider.getName())
|| BCJSSE_PROVIDER_NAME.equals(provider.getName())) {
if (JettyTlsUtil.isJava9AlpnAvailable()) { if (JettyTlsUtil.isJava9AlpnAvailable()) {
return provider; return provider;
} }

View File

@ -60,6 +60,7 @@ import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelFutureListener; import io.netty.channel.ChannelFutureListener;
import io.netty.channel.ChannelHandlerContext; import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelPromise; import io.netty.channel.ChannelPromise;
import io.netty.handler.codec.http2.DecoratingHttp2ConnectionEncoder;
import io.netty.handler.codec.http2.DecoratingHttp2FrameWriter; import io.netty.handler.codec.http2.DecoratingHttp2FrameWriter;
import io.netty.handler.codec.http2.DefaultHttp2Connection; import io.netty.handler.codec.http2.DefaultHttp2Connection;
import io.netty.handler.codec.http2.DefaultHttp2ConnectionDecoder; import io.netty.handler.codec.http2.DefaultHttp2ConnectionDecoder;
@ -83,6 +84,7 @@ import io.netty.handler.codec.http2.Http2FrameWriter;
import io.netty.handler.codec.http2.Http2Headers; import io.netty.handler.codec.http2.Http2Headers;
import io.netty.handler.codec.http2.Http2HeadersDecoder; import io.netty.handler.codec.http2.Http2HeadersDecoder;
import io.netty.handler.codec.http2.Http2InboundFrameLogger; import io.netty.handler.codec.http2.Http2InboundFrameLogger;
import io.netty.handler.codec.http2.Http2LifecycleManager;
import io.netty.handler.codec.http2.Http2OutboundFrameLogger; import io.netty.handler.codec.http2.Http2OutboundFrameLogger;
import io.netty.handler.codec.http2.Http2Settings; import io.netty.handler.codec.http2.Http2Settings;
import io.netty.handler.codec.http2.Http2Stream; import io.netty.handler.codec.http2.Http2Stream;
@ -125,13 +127,11 @@ class NettyServerHandler extends AbstractNettyHandler {
private final long keepAliveTimeoutInNanos; private final long keepAliveTimeoutInNanos;
private final long maxConnectionAgeInNanos; private final long maxConnectionAgeInNanos;
private final long maxConnectionAgeGraceInNanos; private final long maxConnectionAgeGraceInNanos;
private final int maxRstCount; private final RstStreamCounter rstStreamCounter;
private final long maxRstPeriodNanos;
private final List<? extends ServerStreamTracer.Factory> streamTracerFactories; private final List<? extends ServerStreamTracer.Factory> streamTracerFactories;
private final TransportTracer transportTracer; private final TransportTracer transportTracer;
private final KeepAliveEnforcer keepAliveEnforcer; private final KeepAliveEnforcer keepAliveEnforcer;
private final Attributes eagAttributes; private final Attributes eagAttributes;
private final Ticker ticker;
/** Incomplete attributes produced by negotiator. */ /** Incomplete attributes produced by negotiator. */
private Attributes negotiationAttributes; private Attributes negotiationAttributes;
private InternalChannelz.Security securityInfo; private InternalChannelz.Security securityInfo;
@ -149,8 +149,6 @@ class NettyServerHandler extends AbstractNettyHandler {
private ScheduledFuture<?> maxConnectionAgeMonitor; private ScheduledFuture<?> maxConnectionAgeMonitor;
@CheckForNull @CheckForNull
private GracefulShutdown gracefulShutdown; private GracefulShutdown gracefulShutdown;
private int rstCount;
private long lastRstNanoTime;
static NettyServerHandler newHandler( static NettyServerHandler newHandler(
ServerTransportListener transportListener, ServerTransportListener transportListener,
@ -251,6 +249,12 @@ class NettyServerHandler extends AbstractNettyHandler {
final KeepAliveEnforcer keepAliveEnforcer = new KeepAliveEnforcer( final KeepAliveEnforcer keepAliveEnforcer = new KeepAliveEnforcer(
permitKeepAliveWithoutCalls, permitKeepAliveTimeInNanos, TimeUnit.NANOSECONDS); permitKeepAliveWithoutCalls, permitKeepAliveTimeInNanos, TimeUnit.NANOSECONDS);
if (ticker == null) {
ticker = Ticker.systemTicker();
}
RstStreamCounter rstStreamCounter
= new RstStreamCounter(maxRstCount, maxRstPeriodNanos, ticker);
// Create the local flow controller configured to auto-refill the connection window. // Create the local flow controller configured to auto-refill the connection window.
connection.local().flowController( connection.local().flowController(
new DefaultHttp2LocalFlowController(connection, DEFAULT_WINDOW_UPDATE_RATIO, true)); new DefaultHttp2LocalFlowController(connection, DEFAULT_WINDOW_UPDATE_RATIO, true));
@ -258,6 +262,7 @@ class NettyServerHandler extends AbstractNettyHandler {
Http2ConnectionEncoder encoder = Http2ConnectionEncoder encoder =
new DefaultHttp2ConnectionEncoder(connection, frameWriter); new DefaultHttp2ConnectionEncoder(connection, frameWriter);
encoder = new Http2ControlFrameLimitEncoder(encoder, 10000); encoder = new Http2ControlFrameLimitEncoder(encoder, 10000);
encoder = new Http2RstCounterEncoder(encoder, rstStreamCounter);
Http2ConnectionDecoder decoder = new DefaultHttp2ConnectionDecoder(connection, encoder, Http2ConnectionDecoder decoder = new DefaultHttp2ConnectionDecoder(connection, encoder,
frameReader); frameReader);
@ -266,10 +271,6 @@ class NettyServerHandler extends AbstractNettyHandler {
settings.maxConcurrentStreams(maxStreams); settings.maxConcurrentStreams(maxStreams);
settings.maxHeaderListSize(maxHeaderListSize); settings.maxHeaderListSize(maxHeaderListSize);
if (ticker == null) {
ticker = Ticker.systemTicker();
}
return new NettyServerHandler( return new NettyServerHandler(
channelUnused, channelUnused,
connection, connection,
@ -286,8 +287,7 @@ class NettyServerHandler extends AbstractNettyHandler {
maxConnectionAgeInNanos, maxConnectionAgeGraceInNanos, maxConnectionAgeInNanos, maxConnectionAgeGraceInNanos,
keepAliveEnforcer, keepAliveEnforcer,
autoFlowControl, autoFlowControl,
maxRstCount, rstStreamCounter,
maxRstPeriodNanos,
eagAttributes, ticker); eagAttributes, ticker);
} }
@ -310,8 +310,7 @@ class NettyServerHandler extends AbstractNettyHandler {
long maxConnectionAgeGraceInNanos, long maxConnectionAgeGraceInNanos,
final KeepAliveEnforcer keepAliveEnforcer, final KeepAliveEnforcer keepAliveEnforcer,
boolean autoFlowControl, boolean autoFlowControl,
int maxRstCount, RstStreamCounter rstStreamCounter,
long maxRstPeriodNanos,
Attributes eagAttributes, Attributes eagAttributes,
Ticker ticker) { Ticker ticker) {
super( super(
@ -363,12 +362,9 @@ class NettyServerHandler extends AbstractNettyHandler {
this.maxConnectionAgeInNanos = maxConnectionAgeInNanos; this.maxConnectionAgeInNanos = maxConnectionAgeInNanos;
this.maxConnectionAgeGraceInNanos = maxConnectionAgeGraceInNanos; this.maxConnectionAgeGraceInNanos = maxConnectionAgeGraceInNanos;
this.keepAliveEnforcer = checkNotNull(keepAliveEnforcer, "keepAliveEnforcer"); this.keepAliveEnforcer = checkNotNull(keepAliveEnforcer, "keepAliveEnforcer");
this.maxRstCount = maxRstCount; this.rstStreamCounter = rstStreamCounter;
this.maxRstPeriodNanos = maxRstPeriodNanos;
this.eagAttributes = checkNotNull(eagAttributes, "eagAttributes"); this.eagAttributes = checkNotNull(eagAttributes, "eagAttributes");
this.ticker = checkNotNull(ticker, "ticker");
this.lastRstNanoTime = ticker.read();
streamKey = encoder.connection().newKey(); streamKey = encoder.connection().newKey();
this.transportListener = checkNotNull(transportListener, "transportListener"); this.transportListener = checkNotNull(transportListener, "transportListener");
this.streamTracerFactories = checkNotNull(streamTracerFactories, "streamTracerFactories"); this.streamTracerFactories = checkNotNull(streamTracerFactories, "streamTracerFactories");
@ -575,24 +571,9 @@ class NettyServerHandler extends AbstractNettyHandler {
} }
private void onRstStreamRead(int streamId, long errorCode) throws Http2Exception { private void onRstStreamRead(int streamId, long errorCode) throws Http2Exception {
if (maxRstCount > 0) { Http2Exception tooManyRstStream = rstStreamCounter.countRstStream();
long now = ticker.read(); if (tooManyRstStream != null) {
if (now - lastRstNanoTime > maxRstPeriodNanos) { throw tooManyRstStream;
lastRstNanoTime = now;
rstCount = 1;
} else {
rstCount++;
if (rstCount > maxRstCount) {
throw new Http2Exception(Http2Error.ENHANCE_YOUR_CALM, "too_many_rststreams") {
@SuppressWarnings("UnsynchronizedOverridesSynchronized") // No memory accesses
@Override
public Throwable fillInStackTrace() {
// Avoid the CPU cycles, since the resets may be a CPU consumption attack
return this;
}
};
}
}
} }
try { try {
@ -1180,6 +1161,81 @@ class NettyServerHandler extends AbstractNettyHandler {
} }
} }
private static final class Http2RstCounterEncoder extends DecoratingHttp2ConnectionEncoder {
private final RstStreamCounter rstStreamCounter;
private Http2LifecycleManager lifecycleManager;
Http2RstCounterEncoder(Http2ConnectionEncoder encoder, RstStreamCounter rstStreamCounter) {
super(encoder);
this.rstStreamCounter = rstStreamCounter;
}
@Override
public void lifecycleManager(Http2LifecycleManager lifecycleManager) {
this.lifecycleManager = lifecycleManager;
super.lifecycleManager(lifecycleManager);
}
@Override
public ChannelFuture writeRstStream(
ChannelHandlerContext ctx, int streamId, long errorCode, ChannelPromise promise) {
ChannelFuture future = super.writeRstStream(ctx, streamId, errorCode, promise);
// We want to count "induced" RST_STREAM, where the server sent a reset because of a malformed
// frame.
boolean normalRst
= errorCode == Http2Error.NO_ERROR.code() || errorCode == Http2Error.CANCEL.code();
if (!normalRst) {
Http2Exception tooManyRstStream = rstStreamCounter.countRstStream();
if (tooManyRstStream != null) {
lifecycleManager.onError(ctx, true, tooManyRstStream);
ctx.close();
}
}
return future;
}
}
private static final class RstStreamCounter {
private final int maxRstCount;
private final long maxRstPeriodNanos;
private final Ticker ticker;
private int rstCount;
private long lastRstNanoTime;
RstStreamCounter(int maxRstCount, long maxRstPeriodNanos, Ticker ticker) {
checkArgument(maxRstCount >= 0, "maxRstCount must be non-negative: %s", maxRstCount);
this.maxRstCount = maxRstCount;
this.maxRstPeriodNanos = maxRstPeriodNanos;
this.ticker = checkNotNull(ticker, "ticker");
this.lastRstNanoTime = ticker.read();
}
/** Returns non-{@code null} when the connection should be killed by the caller. */
private Http2Exception countRstStream() {
if (maxRstCount == 0) {
return null;
}
long now = ticker.read();
if (now - lastRstNanoTime > maxRstPeriodNanos) {
lastRstNanoTime = now;
rstCount = 1;
} else {
rstCount++;
if (rstCount > maxRstCount) {
return new Http2Exception(Http2Error.ENHANCE_YOUR_CALM, "too_many_rststreams") {
@SuppressWarnings("UnsynchronizedOverridesSynchronized") // No memory accesses
@Override
public Throwable fillInStackTrace() {
// Avoid the CPU cycles, since the resets may be a CPU consumption attack
return this;
}
};
}
}
return null;
}
}
private static class ServerChannelLogger extends ChannelLogger { private static class ServerChannelLogger extends ChannelLogger {
private static final Logger log = Logger.getLogger(ChannelLogger.class.getName()); private static final Logger log = Logger.getLogger(ChannelLogger.class.getName());

View File

@ -28,7 +28,6 @@ import static io.grpc.netty.Utils.HTTP_METHOD;
import static io.grpc.netty.Utils.STATUS_OK; import static io.grpc.netty.Utils.STATUS_OK;
import static io.grpc.netty.Utils.TE_HEADER; import static io.grpc.netty.Utils.TE_HEADER;
import static io.grpc.netty.Utils.TE_TRAILERS; import static io.grpc.netty.Utils.TE_TRAILERS;
import static io.netty.handler.codec.http2.Http2CodecUtil.DEFAULT_PRIORITY_WEIGHT;
import static java.nio.charset.StandardCharsets.UTF_8; import static java.nio.charset.StandardCharsets.UTF_8;
import static org.junit.Assert.assertArrayEquals; import static org.junit.Assert.assertArrayEquals;
import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertEquals;
@ -276,7 +275,7 @@ public class NettyClientHandlerTest extends NettyHandlerTestBase<NettyClientHand
public void createStreamShouldSucceed() throws Exception { public void createStreamShouldSucceed() throws Exception {
createStream(); createStream();
verifyWrite().writeHeaders(eq(ctx()), eq(STREAM_ID), eq(grpcHeaders), eq(0), verifyWrite().writeHeaders(eq(ctx()), eq(STREAM_ID), eq(grpcHeaders), eq(0),
eq(DEFAULT_PRIORITY_WEIGHT), eq(false), eq(0), eq(false), any(ChannelPromise.class)); eq(false), any(ChannelPromise.class));
} }
@Test @Test

View File

@ -1304,6 +1304,8 @@ public class NettyServerHandlerTest extends NettyHandlerTestBase<NettyServerHand
} }
private void rapidReset(int burstSize) throws Exception { private void rapidReset(int burstSize) throws Exception {
when(streamTracerFactory.newServerStreamTracer(anyString(), any(Metadata.class)))
.thenAnswer((args) -> new TestServerStreamTracer());
Http2Headers headers = new DefaultHttp2Headers() Http2Headers headers = new DefaultHttp2Headers()
.method(HTTP_METHOD) .method(HTTP_METHOD)
.set(CONTENT_TYPE_HEADER, new AsciiString("application/grpc", UTF_8)) .set(CONTENT_TYPE_HEADER, new AsciiString("application/grpc", UTF_8))
@ -1323,6 +1325,48 @@ public class NettyServerHandlerTest extends NettyHandlerTestBase<NettyServerHand
} }
} }
@Test
public void maxRstCountSent_withinLimit_succeeds() throws Exception {
maxRstCount = 10;
maxRstPeriodNanos = TimeUnit.MILLISECONDS.toNanos(100);
manualSetUp();
madeYouReset(maxRstCount);
assertTrue(channel().isOpen());
}
@Test
public void maxRstCountSent_exceedsLimit_fails() throws Exception {
maxRstCount = 10;
maxRstPeriodNanos = TimeUnit.MILLISECONDS.toNanos(100);
manualSetUp();
assertThrows(ClosedChannelException.class, () -> madeYouReset(maxRstCount + 1));
assertFalse(channel().isOpen());
}
private void madeYouReset(int burstSize) throws Exception {
when(streamTracerFactory.newServerStreamTracer(anyString(), any(Metadata.class)))
.thenAnswer((args) -> new TestServerStreamTracer());
Http2Headers headers = new DefaultHttp2Headers()
.method(HTTP_METHOD)
.set(CONTENT_TYPE_HEADER, new AsciiString("application/grpc", UTF_8))
.set(TE_HEADER, TE_TRAILERS)
.path(new AsciiString("/foo/bar"));
int streamId = 1;
long rpcTimeNanos = maxRstPeriodNanos / 2 / burstSize;
for (int period = 0; period < 3; period++) {
for (int i = 0; i < burstSize; i++) {
channelRead(headersFrame(streamId, headers));
channelRead(windowUpdate(streamId, 0));
streamId += 2;
fakeClock().forwardNanos(rpcTimeNanos);
}
while (channel().readOutbound() != null) {}
fakeClock().forwardNanos(maxRstPeriodNanos - rpcTimeNanos * burstSize + 1);
}
}
private void createStream() throws Exception { private void createStream() throws Exception {
Http2Headers headers = new DefaultHttp2Headers() Http2Headers headers = new DefaultHttp2Headers()
.method(HTTP_METHOD) .method(HTTP_METHOD)

View File

@ -12,7 +12,7 @@ load("@bazel_tools//tools/build_defs/repo:http.bzl", "http_archive")
# GRPC_DEPS_START # GRPC_DEPS_START
IO_GRPC_GRPC_JAVA_ARTIFACTS = [ IO_GRPC_GRPC_JAVA_ARTIFACTS = [
"com.google.android:annotations:4.1.1.4", "com.google.android:annotations:4.1.1.4",
"com.google.api.grpc:proto-google-common-protos:2.51.0", "com.google.api.grpc:proto-google-common-protos:2.59.2",
"com.google.auth:google-auth-library-credentials:1.24.1", "com.google.auth:google-auth-library-credentials:1.24.1",
"com.google.auth:google-auth-library-oauth2-http:1.24.1", "com.google.auth:google-auth-library-oauth2-http:1.24.1",
"com.google.auto.value:auto-value-annotations:1.11.0", "com.google.auto.value:auto-value-annotations:1.11.0",
@ -23,24 +23,24 @@ IO_GRPC_GRPC_JAVA_ARTIFACTS = [
"com.google.guava:failureaccess:1.0.1", "com.google.guava:failureaccess:1.0.1",
"com.google.guava:guava:33.3.1-android", "com.google.guava:guava:33.3.1-android",
"com.google.re2j:re2j:1.8", "com.google.re2j:re2j:1.8",
"com.google.s2a.proto.v2:s2a-proto:0.1.1", "com.google.s2a.proto.v2:s2a-proto:0.1.2",
"com.google.truth:truth:1.4.2", "com.google.truth:truth:1.4.2",
"com.squareup.okhttp:okhttp:2.7.5", "com.squareup.okhttp:okhttp:2.7.5",
"com.squareup.okio:okio:2.10.0", # 3.0+ needs swapping to -jvm; need work to avoid flag-day "com.squareup.okio:okio:2.10.0", # 3.0+ needs swapping to -jvm; need work to avoid flag-day
"io.netty:netty-buffer:4.1.110.Final", "io.netty:netty-buffer:4.1.124.Final",
"io.netty:netty-codec-http2:4.1.110.Final", "io.netty:netty-codec-http2:4.1.124.Final",
"io.netty:netty-codec-http:4.1.110.Final", "io.netty:netty-codec-http:4.1.124.Final",
"io.netty:netty-codec-socks:4.1.110.Final", "io.netty:netty-codec-socks:4.1.124.Final",
"io.netty:netty-codec:4.1.110.Final", "io.netty:netty-codec:4.1.124.Final",
"io.netty:netty-common:4.1.110.Final", "io.netty:netty-common:4.1.124.Final",
"io.netty:netty-handler-proxy:4.1.110.Final", "io.netty:netty-handler-proxy:4.1.124.Final",
"io.netty:netty-handler:4.1.110.Final", "io.netty:netty-handler:4.1.124.Final",
"io.netty:netty-resolver:4.1.110.Final", "io.netty:netty-resolver:4.1.124.Final",
"io.netty:netty-tcnative-boringssl-static:2.0.70.Final", "io.netty:netty-tcnative-boringssl-static:2.0.70.Final",
"io.netty:netty-tcnative-classes:2.0.70.Final", "io.netty:netty-tcnative-classes:2.0.70.Final",
"io.netty:netty-transport-native-epoll:jar:linux-x86_64:4.1.110.Final", "io.netty:netty-transport-native-epoll:jar:linux-x86_64:4.1.124.Final",
"io.netty:netty-transport-native-unix-common:4.1.110.Final", "io.netty:netty-transport-native-unix-common:4.1.124.Final",
"io.netty:netty-transport:4.1.110.Final", "io.netty:netty-transport:4.1.124.Final",
"io.opencensus:opencensus-api:0.31.0", "io.opencensus:opencensus-api:0.31.0",
"io.opencensus:opencensus-contrib-grpc-metrics:0.31.0", "io.opencensus:opencensus-contrib-grpc-metrics:0.31.0",
"io.perfmark:perfmark-api:0.27.0", "io.perfmark:perfmark-api:0.27.0",
@ -116,9 +116,9 @@ def com_google_protobuf():
# This statement defines the @com_google_protobuf repo. # This statement defines the @com_google_protobuf repo.
http_archive( http_archive(
name = "com_google_protobuf", name = "com_google_protobuf",
sha256 = "9bd87b8280ef720d3240514f884e56a712f2218f0d693b48050c836028940a42", sha256 = "3cf7d5b17c4ff04fe9f038104e9d0cae6da09b8ce271c13e44f8ac69f51e4e0f",
strip_prefix = "protobuf-25.1", strip_prefix = "protobuf-25.5",
urls = ["https://github.com/protocolbuffers/protobuf/releases/download/v25.1/protobuf-25.1.tar.gz"], urls = ["https://github.com/protocolbuffers/protobuf/releases/download/v25.5/protobuf-25.5.tar.gz"],
) )
def io_grpc_grpc_proto(): def io_grpc_grpc_proto():

View File

@ -29,6 +29,7 @@ import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue; import java.util.concurrent.BlockingQueue;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException; import java.util.concurrent.TimeoutException;
import java.util.concurrent.atomic.AtomicReference;
import java.util.logging.Level; import java.util.logging.Level;
import java.util.logging.Logger; import java.util.logging.Logger;
@ -69,7 +70,7 @@ public final class BlockingClientCall<ReqT, RespT> {
private final ThreadSafeThreadlessExecutor executor; private final ThreadSafeThreadlessExecutor executor;
private boolean writeClosed; private boolean writeClosed;
private volatile Status closedStatus; // null if not closed private AtomicReference<CloseState> closeState = new AtomicReference<>();
BlockingClientCall(ClientCall<ReqT, RespT> call, ThreadSafeThreadlessExecutor executor) { BlockingClientCall(ClientCall<ReqT, RespT> call, ThreadSafeThreadlessExecutor executor) {
this.call = call; this.call = call;
@ -120,22 +121,22 @@ public final class BlockingClientCall<ReqT, RespT> {
logger.finer("Client Blocking read had value: " + bufferedValue); logger.finer("Client Blocking read had value: " + bufferedValue);
} }
Status currentClosedStatus; CloseState currentCloseState;
if (bufferedValue != null) { if (bufferedValue != null) {
call.request(1); call.request(1);
return bufferedValue; return bufferedValue;
} else if ((currentClosedStatus = closedStatus) == null) { } else if ((currentCloseState = closeState.get()) == null) {
throw new IllegalStateException( throw new IllegalStateException(
"The message disappeared... are you reading from multiple threads?"); "The message disappeared... are you reading from multiple threads?");
} else if (!currentClosedStatus.isOk()) { } else if (!currentCloseState.status.isOk()) {
throw currentClosedStatus.asException(); throw currentCloseState.status.asException(currentCloseState.trailers);
} else { } else {
return null; return null;
} }
} }
boolean skipWaitingForRead() { boolean skipWaitingForRead() {
return closedStatus != null || !buffer.isEmpty(); return closeState.get() != null || !buffer.isEmpty();
} }
/** /**
@ -148,11 +149,11 @@ public final class BlockingClientCall<ReqT, RespT> {
* @throws StatusException If the stream was closed in an error state * @throws StatusException If the stream was closed in an error state
*/ */
public boolean hasNext() throws InterruptedException, StatusException { public boolean hasNext() throws InterruptedException, StatusException {
executor.waitAndDrain((x) -> !x.buffer.isEmpty() || x.closedStatus != null, this); executor.waitAndDrain((x) -> !x.buffer.isEmpty() || x.closeState.get() != null, this);
Status currentClosedStatus = closedStatus; CloseState currentCloseState = closeState.get();
if (currentClosedStatus != null && !currentClosedStatus.isOk()) { if (currentCloseState != null && !currentCloseState.status.isOk()) {
throw currentClosedStatus.asException(); throw currentCloseState.status.asException(currentCloseState.trailers);
} }
return !buffer.isEmpty(); return !buffer.isEmpty();
@ -221,17 +222,16 @@ public final class BlockingClientCall<ReqT, RespT> {
} }
Predicate<BlockingClientCall<ReqT, RespT>> predicate = Predicate<BlockingClientCall<ReqT, RespT>> predicate =
(x) -> x.call.isReady() || x.closedStatus != null; (x) -> x.call.isReady() || x.closeState.get() != null;
executor.waitAndDrainWithTimeout(waitForever, endNanoTime, predicate, this); executor.waitAndDrainWithTimeout(waitForever, endNanoTime, predicate, this);
Status savedClosedStatus = closedStatus; CloseState savedCloseState = closeState.get();
if (savedClosedStatus == null) { if (savedCloseState == null || savedCloseState.status == null) {
call.sendMessage(request); call.sendMessage(request);
return true; return true;
} else if (savedClosedStatus.isOk()) { } else if (savedCloseState.status.isOk()) {
return false; return false;
} else { } else {
// Propagate any errors returned from the server throw savedCloseState.status.asException(savedCloseState.trailers);
throw savedClosedStatus.asException();
} }
} }
@ -274,7 +274,8 @@ public final class BlockingClientCall<ReqT, RespT> {
@VisibleForTesting @VisibleForTesting
Status getClosedStatus() { Status getClosedStatus() {
drainQuietly(); drainQuietly();
return closedStatus; CloseState state = closeState.get();
return (state == null) ? null : state.status;
} }
/** /**
@ -317,7 +318,7 @@ public final class BlockingClientCall<ReqT, RespT> {
* @return True if writes haven't been closed and the server hasn't closed the stream * @return True if writes haven't been closed and the server hasn't closed the stream
*/ */
private boolean isWriteLegal() { private boolean isWriteLegal() {
return !writeClosed && closedStatus == null; return !writeClosed && closeState.get() == null;
} }
ClientCall.Listener<RespT> getListener() { ClientCall.Listener<RespT> getListener() {
@ -335,15 +336,25 @@ public final class BlockingClientCall<ReqT, RespT> {
private final class QueuingListener extends ClientCall.Listener<RespT> { private final class QueuingListener extends ClientCall.Listener<RespT> {
@Override @Override
public void onMessage(RespT value) { public void onMessage(RespT value) {
Preconditions.checkState(closedStatus == null, "ClientCall already closed"); Preconditions.checkState(closeState.get() == null, "ClientCall already closed");
buffer.add(value); buffer.add(value);
} }
@Override @Override
public void onClose(Status status, Metadata trailers) { public void onClose(Status status, Metadata trailers) {
Preconditions.checkState(closedStatus == null, "ClientCall already closed"); CloseState newCloseState = new CloseState(status, trailers);
closedStatus = status; boolean wasSet = closeState.compareAndSet(null, newCloseState);
Preconditions.checkState(wasSet, "ClientCall already closed");
} }
} }
private static final class CloseState {
final Status status;
final Metadata trailers;
CloseState(Status status, Metadata trailers) {
this.status = Preconditions.checkNotNull(status, "status");
this.trailers = trailers;
}
}
} }

View File

@ -320,13 +320,14 @@ final class PriorityLoadBalancer extends LoadBalancer {
if (!children.containsKey(priority)) { if (!children.containsKey(priority)) {
return; return;
} }
ConnectivityState oldState = connectivityState;
connectivityState = newState; connectivityState = newState;
picker = newPicker; picker = newPicker;
if (deletionTimer != null && deletionTimer.isPending()) { if (deletionTimer != null && deletionTimer.isPending()) {
return; return;
} }
if (newState.equals(CONNECTING)) { if (newState.equals(CONNECTING) && !oldState.equals(newState)) {
if (!failOverTimer.isPending() && seenReadyOrIdleSinceTransientFailure) { if (!failOverTimer.isPending() && seenReadyOrIdleSinceTransientFailure) {
failOverTimer = syncContext.schedule(new FailOverTask(), 10, TimeUnit.SECONDS, failOverTimer = syncContext.schedule(new FailOverTask(), 10, TimeUnit.SECONDS,
executor); executor);

View File

@ -48,6 +48,7 @@ import java.util.ArrayList;
import java.util.Collection; import java.util.Collection;
import java.util.HashSet; import java.util.HashSet;
import java.util.List; import java.util.List;
import java.util.Objects;
import java.util.Random; import java.util.Random;
import java.util.Set; import java.util.Set;
import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.ScheduledExecutorService;
@ -738,6 +739,32 @@ final class WeightedRoundRobinLoadBalancer extends MultiChildLoadBalancer {
this.errorUtilizationPenalty = errorUtilizationPenalty; this.errorUtilizationPenalty = errorUtilizationPenalty;
} }
@Override
public boolean equals(Object o) {
if (!(o instanceof WeightedRoundRobinLoadBalancerConfig)) {
return false;
}
WeightedRoundRobinLoadBalancerConfig that = (WeightedRoundRobinLoadBalancerConfig) o;
return this.blackoutPeriodNanos == that.blackoutPeriodNanos
&& this.weightExpirationPeriodNanos == that.weightExpirationPeriodNanos
&& this.enableOobLoadReport == that.enableOobLoadReport
&& this.oobReportingPeriodNanos == that.oobReportingPeriodNanos
&& this.weightUpdatePeriodNanos == that.weightUpdatePeriodNanos
// Float.compare considers NaNs equal
&& Float.compare(this.errorUtilizationPenalty, that.errorUtilizationPenalty) == 0;
}
@Override
public int hashCode() {
return Objects.hash(
blackoutPeriodNanos,
weightExpirationPeriodNanos,
enableOobLoadReport,
oobReportingPeriodNanos,
weightUpdatePeriodNanos,
errorUtilizationPenalty);
}
static final class Builder { static final class Builder {
long blackoutPeriodNanos = 10_000_000_000L; // 10s long blackoutPeriodNanos = 10_000_000_000L; // 10s
long weightExpirationPeriodNanos = 180_000_000_000L; //3min long weightExpirationPeriodNanos = 180_000_000_000L; //3min

View File

@ -109,7 +109,7 @@ final class ControlPlaneClient {
this.backoffPolicyProvider = checkNotNull(backoffPolicyProvider, "backoffPolicyProvider"); this.backoffPolicyProvider = checkNotNull(backoffPolicyProvider, "backoffPolicyProvider");
this.messagePrinter = checkNotNull(messagePrinter, "messagePrinter"); this.messagePrinter = checkNotNull(messagePrinter, "messagePrinter");
stopwatch = checkNotNull(stopwatchSupplier, "stopwatchSupplier").get(); stopwatch = checkNotNull(stopwatchSupplier, "stopwatchSupplier").get();
logId = InternalLogId.allocate("xds-client", serverInfo.target()); logId = InternalLogId.allocate("xds-cp-client", serverInfo.target());
logger = XdsLogger.withLogId(logId); logger = XdsLogger.withLogId(logId);
logger.log(XdsLogLevel.INFO, "Created"); logger.log(XdsLogLevel.INFO, "Created");
} }

View File

@ -676,6 +676,8 @@ public final class XdsClientImpl extends XdsClient implements ResourceStore {
private ResourceMetadata metadata; private ResourceMetadata metadata;
@Nullable @Nullable
private String errorDescription; private String errorDescription;
@Nullable
private Status lastError;
ResourceSubscriber(XdsResourceType<T> type, String resource) { ResourceSubscriber(XdsResourceType<T> type, String resource) {
syncContext.throwIfNotInThisSynchronizationContext(); syncContext.throwIfNotInThisSynchronizationContext();
@ -712,11 +714,16 @@ public final class XdsClientImpl extends XdsClient implements ResourceStore {
watchers.put(watcher, watcherExecutor); watchers.put(watcher, watcherExecutor);
T savedData = data; T savedData = data;
boolean savedAbsent = absent; boolean savedAbsent = absent;
Status savedError = lastError;
watcherExecutor.execute(() -> { watcherExecutor.execute(() -> {
if (errorDescription != null) { if (errorDescription != null) {
watcher.onError(Status.INVALID_ARGUMENT.withDescription(errorDescription)); watcher.onError(Status.INVALID_ARGUMENT.withDescription(errorDescription));
return; return;
} }
if (savedError != null) {
watcher.onError(savedError);
return;
}
if (savedData != null) { if (savedData != null) {
notifyWatcher(watcher, savedData); notifyWatcher(watcher, savedData);
} else if (savedAbsent) { } else if (savedAbsent) {
@ -808,6 +815,7 @@ public final class XdsClientImpl extends XdsClient implements ResourceStore {
this.metadata = ResourceMetadata this.metadata = ResourceMetadata
.newResourceMetadataAcked(parsedResource.getRawResource(), version, updateTime); .newResourceMetadataAcked(parsedResource.getRawResource(), version, updateTime);
absent = false; absent = false;
lastError = null;
if (resourceDeletionIgnored) { if (resourceDeletionIgnored) {
logger.log(XdsLogLevel.FORCE_INFO, "xds server {0}: server returned new version " logger.log(XdsLogLevel.FORCE_INFO, "xds server {0}: server returned new version "
+ "of resource for which we previously ignored a deletion: type {1} name {2}", + "of resource for which we previously ignored a deletion: type {1} name {2}",
@ -857,6 +865,7 @@ public final class XdsClientImpl extends XdsClient implements ResourceStore {
if (!absent) { if (!absent) {
data = null; data = null;
absent = true; absent = true;
lastError = null;
metadata = serverInfo.resourceTimerIsTransientError() metadata = serverInfo.resourceTimerIsTransientError()
? ResourceMetadata.newResourceMetadataTimeout() ? ResourceMetadata.newResourceMetadataTimeout()
: ResourceMetadata.newResourceMetadataDoesNotExist(); : ResourceMetadata.newResourceMetadataDoesNotExist();
@ -894,6 +903,7 @@ public final class XdsClientImpl extends XdsClient implements ResourceStore {
Status errorAugmented = Status.fromCode(error.getCode()) Status errorAugmented = Status.fromCode(error.getCode())
.withDescription(description + "nodeID: " + bootstrapInfo.node().getId()) .withDescription(description + "nodeID: " + bootstrapInfo.node().getId())
.withCause(error.getCause()); .withCause(error.getCause());
this.lastError = errorAugmented;
for (ResourceWatcher<T> watcher : watchers.keySet()) { for (ResourceWatcher<T> watcher : watchers.keySet()) {
if (tracker != null) { if (tracker != null) {

View File

@ -285,6 +285,8 @@ public abstract class GrpcXdsClientImplTestBase {
@Mock @Mock
private ResourceWatcher<LdsUpdate> ldsResourceWatcher; private ResourceWatcher<LdsUpdate> ldsResourceWatcher;
@Mock @Mock
private ResourceWatcher<LdsUpdate> ldsResourceWatcher2;
@Mock
private ResourceWatcher<RdsUpdate> rdsResourceWatcher; private ResourceWatcher<RdsUpdate> rdsResourceWatcher;
@Mock @Mock
private ResourceWatcher<CdsUpdate> cdsResourceWatcher; private ResourceWatcher<CdsUpdate> cdsResourceWatcher;
@ -694,6 +696,24 @@ public abstract class GrpcXdsClientImplTestBase {
assertThat(resourceDiscoveryCalls.poll()).isNull(); assertThat(resourceDiscoveryCalls.poll()).isNull();
} }
@Test
public void ldsResource_onError_cachedForNewWatcher() {
xdsClient.watchXdsResource(XdsListenerResource.getInstance(), LDS_RESOURCE,
ldsResourceWatcher);
DiscoveryRpcCall call = resourceDiscoveryCalls.poll();
call.sendCompleted();
verify(ldsResourceWatcher).onError(errorCaptor.capture());
Status initialError = errorCaptor.getValue();
xdsClient.watchXdsResource(XdsListenerResource.getInstance(), LDS_RESOURCE,
ldsResourceWatcher2);
ArgumentCaptor<Status> secondErrorCaptor = ArgumentCaptor.forClass(Status.class);
verify(ldsResourceWatcher2).onError(secondErrorCaptor.capture());
Status cachedError = secondErrorCaptor.getValue();
assertThat(cachedError).isEqualTo(initialError);
assertThat(resourceDiscoveryCalls.poll()).isNull();
}
@Test @Test
public void ldsResponseErrorHandling_allResourcesFailedUnpack() { public void ldsResponseErrorHandling_allResourcesFailedUnpack() {
DiscoveryRpcCall call = startResourceWatcher(XdsListenerResource.getInstance(), LDS_RESOURCE, DiscoveryRpcCall call = startResourceWatcher(XdsListenerResource.getInstance(), LDS_RESOURCE,

View File

@ -28,6 +28,7 @@ import static org.mockito.ArgumentMatchers.isA;
import static org.mockito.Mockito.atLeastOnce; import static org.mockito.Mockito.atLeastOnce;
import static org.mockito.Mockito.clearInvocations; import static org.mockito.Mockito.clearInvocations;
import static org.mockito.Mockito.doReturn; import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.inOrder;
import static org.mockito.Mockito.mock; import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never; import static org.mockito.Mockito.never;
import static org.mockito.Mockito.times; import static org.mockito.Mockito.times;
@ -70,6 +71,7 @@ import org.junit.runner.RunWith;
import org.junit.runners.JUnit4; import org.junit.runners.JUnit4;
import org.mockito.ArgumentCaptor; import org.mockito.ArgumentCaptor;
import org.mockito.Captor; import org.mockito.Captor;
import org.mockito.InOrder;
import org.mockito.Mock; import org.mockito.Mock;
import org.mockito.junit.MockitoJUnit; import org.mockito.junit.MockitoJUnit;
import org.mockito.junit.MockitoRule; import org.mockito.junit.MockitoRule;
@ -554,6 +556,55 @@ public class PriorityLoadBalancerTest {
assertThat(fooHelpers).hasSize(2); assertThat(fooHelpers).hasSize(2);
} }
@Test
public void failoverTimerNotRestartedOnDupConnecting() {
InOrder inOrder = inOrder(helper);
PriorityChildConfig priorityChildConfig0 =
new PriorityChildConfig(newChildConfig(fooLbProvider, new Object()), true);
PriorityChildConfig priorityChildConfig1 =
new PriorityChildConfig(newChildConfig(fooLbProvider, new Object()), true);
PriorityLbConfig priorityLbConfig =
new PriorityLbConfig(
ImmutableMap.of("p0", priorityChildConfig0, "p1", priorityChildConfig1),
ImmutableList.of("p0", "p1"));
priorityLb.acceptResolvedAddresses(
ResolvedAddresses.newBuilder()
.setAddresses(ImmutableList.<EquivalentAddressGroup>of())
.setLoadBalancingPolicyConfig(priorityLbConfig)
.build());
// Nothing important about this verify, other than to provide a baseline
inOrder.verify(helper)
.updateBalancingState(eq(CONNECTING), pickerReturns(PickResult.withNoResult()));
assertThat(fooBalancers).hasSize(1);
assertThat(fooHelpers).hasSize(1);
Helper helper0 = Iterables.getOnlyElement(fooHelpers);
// Cause seenReadyOrIdleSinceTransientFailure = true
helper0.updateBalancingState(IDLE, EMPTY_PICKER);
inOrder.verify(helper)
.updateBalancingState(eq(IDLE), pickerReturns(PickResult.withNoResult()));
helper0.updateBalancingState(CONNECTING, EMPTY_PICKER);
// p0 keeps repeating CONNECTING, failover happens
fakeClock.forwardTime(5, TimeUnit.SECONDS);
helper0.updateBalancingState(CONNECTING, EMPTY_PICKER);
fakeClock.forwardTime(5, TimeUnit.SECONDS);
assertThat(fooBalancers).hasSize(2);
assertThat(fooHelpers).hasSize(2);
inOrder.verify(helper, times(2))
.updateBalancingState(eq(CONNECTING), pickerReturns(PickResult.withNoResult()));
Helper helper1 = Iterables.getLast(fooHelpers);
// p0 keeps repeating CONNECTING, no reset of failover timer
helper1.updateBalancingState(IDLE, EMPTY_PICKER); // Stop timer for p1
inOrder.verify(helper)
.updateBalancingState(eq(IDLE), pickerReturns(PickResult.withNoResult()));
helper0.updateBalancingState(CONNECTING, EMPTY_PICKER);
fakeClock.forwardTime(10, TimeUnit.SECONDS);
inOrder.verify(helper, never())
.updateBalancingState(eq(CONNECTING), any());
}
@Test @Test
public void readyToConnectDoesNotFailOverButUpdatesPicker() { public void readyToConnectDoesNotFailOverButUpdatesPicker() {
PriorityChildConfig priorityChildConfig0 = PriorityChildConfig priorityChildConfig0 =

View File

@ -35,6 +35,7 @@ import com.github.xds.service.orca.v3.OrcaLoadReportRequest;
import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableMap;
import com.google.common.collect.Lists; import com.google.common.collect.Lists;
import com.google.common.collect.Maps; import com.google.common.collect.Maps;
import com.google.common.testing.EqualsTester;
import com.google.protobuf.Duration; import com.google.protobuf.Duration;
import io.grpc.Attributes; import io.grpc.Attributes;
import io.grpc.CallOptions; import io.grpc.CallOptions;
@ -215,6 +216,40 @@ public class WeightedRoundRobinLoadBalancerTest {
weightedPicker.pickSubchannel(mockArgs); weightedPicker.pickSubchannel(mockArgs);
} }
@Test
public void config_equalsTester() {
WeightedRoundRobinLoadBalancerConfig defaults =
WeightedRoundRobinLoadBalancerConfig.newBuilder().build();
new EqualsTester()
.addEqualityGroup(
WeightedRoundRobinLoadBalancerConfig.newBuilder().build(),
WeightedRoundRobinLoadBalancerConfig.newBuilder().build(),
WeightedRoundRobinLoadBalancerConfig.newBuilder()
.setBlackoutPeriodNanos(defaults.blackoutPeriodNanos).build())
.addEqualityGroup(
WeightedRoundRobinLoadBalancerConfig.newBuilder()
.setBlackoutPeriodNanos(5).build())
.addEqualityGroup(
WeightedRoundRobinLoadBalancerConfig.newBuilder()
.setWeightExpirationPeriodNanos(5).build())
.addEqualityGroup(
WeightedRoundRobinLoadBalancerConfig.newBuilder()
.setEnableOobLoadReport(true).build())
.addEqualityGroup(
WeightedRoundRobinLoadBalancerConfig.newBuilder()
.setOobReportingPeriodNanos(5).build())
.addEqualityGroup(
WeightedRoundRobinLoadBalancerConfig.newBuilder()
.setWeightUpdatePeriodNanos(5).build())
.addEqualityGroup(
WeightedRoundRobinLoadBalancerConfig.newBuilder()
.setErrorUtilizationPenalty(0.5F).build())
.addEqualityGroup(
WeightedRoundRobinLoadBalancerConfig.newBuilder()
.setErrorUtilizationPenalty(Float.NaN).build())
.testEquals();
}
@Test @Test
public void wrrLifeCycle() { public void wrrLifeCycle() {
syncContext.execute(() -> wrr.acceptResolvedAddresses(ResolvedAddresses.newBuilder() syncContext.execute(() -> wrr.acceptResolvedAddresses(ResolvedAddresses.newBuilder()