mirror of https://github.com/grpc/grpc-java.git
master pull
This commit is contained in:
commit
5bb1ef5786
|
@ -11,8 +11,6 @@ for general contribution guidelines.
|
||||||
- [ejona86](https://github.com/ejona86), Google LLC
|
- [ejona86](https://github.com/ejona86), Google LLC
|
||||||
- [jdcormie](https://github.com/jdcormie), Google LLC
|
- [jdcormie](https://github.com/jdcormie), Google LLC
|
||||||
- [kannanjgithub](https://github.com/kannanjgithub), Google LLC
|
- [kannanjgithub](https://github.com/kannanjgithub), Google LLC
|
||||||
- [larry-safran](https://github.com/larry-safran), Google LLC
|
|
||||||
- [markb74](https://github.com/markb74), Google LLC
|
|
||||||
- [ran-su](https://github.com/ran-su), Google LLC
|
- [ran-su](https://github.com/ran-su), Google LLC
|
||||||
- [sergiitk](https://github.com/sergiitk), Google LLC
|
- [sergiitk](https://github.com/sergiitk), Google LLC
|
||||||
- [temawi](https://github.com/temawi), Google LLC
|
- [temawi](https://github.com/temawi), Google LLC
|
||||||
|
@ -26,7 +24,9 @@ for general contribution guidelines.
|
||||||
- [ericgribkoff](https://github.com/ericgribkoff)
|
- [ericgribkoff](https://github.com/ericgribkoff)
|
||||||
- [jiangtaoli2016](https://github.com/jiangtaoli2016)
|
- [jiangtaoli2016](https://github.com/jiangtaoli2016)
|
||||||
- [jtattermusch](https://github.com/jtattermusch)
|
- [jtattermusch](https://github.com/jtattermusch)
|
||||||
|
- [larry-safran](https://github.com/larry-safran)
|
||||||
- [louiscryan](https://github.com/louiscryan)
|
- [louiscryan](https://github.com/louiscryan)
|
||||||
|
- [markb74](https://github.com/markb74)
|
||||||
- [nicolasnoble](https://github.com/nicolasnoble)
|
- [nicolasnoble](https://github.com/nicolasnoble)
|
||||||
- [nmittler](https://github.com/nmittler)
|
- [nmittler](https://github.com/nmittler)
|
||||||
- [sanjaypujare](https://github.com/sanjaypujare)
|
- [sanjaypujare](https://github.com/sanjaypujare)
|
||||||
|
|
|
@ -239,6 +239,9 @@ public abstract class NameResolver {
|
||||||
* {@link ResolutionResult#getAddressesOrError()} is empty, {@link #onError(Status)} will be
|
* {@link ResolutionResult#getAddressesOrError()} is empty, {@link #onError(Status)} will be
|
||||||
* called.
|
* called.
|
||||||
*
|
*
|
||||||
|
* <p>Newer NameResolver implementations should prefer calling onResult2. This method exists to
|
||||||
|
* facilitate older {@link Listener} implementations to migrate to {@link Listener2}.
|
||||||
|
*
|
||||||
* @param resolutionResult the resolved server addresses, attributes, and Service Config.
|
* @param resolutionResult the resolved server addresses, attributes, and Service Config.
|
||||||
* @since 1.21.0
|
* @since 1.21.0
|
||||||
*/
|
*/
|
||||||
|
@ -248,6 +251,10 @@ public abstract class NameResolver {
|
||||||
* Handles a name resolving error from the resolver. The listener is responsible for eventually
|
* Handles a name resolving error from the resolver. The listener is responsible for eventually
|
||||||
* invoking {@link NameResolver#refresh()} to re-attempt resolution.
|
* invoking {@link NameResolver#refresh()} to re-attempt resolution.
|
||||||
*
|
*
|
||||||
|
* <p>New NameResolver implementations should prefer calling onResult2 which will have the
|
||||||
|
* address resolution error in {@link ResolutionResult}'s addressesOrError. This method exists
|
||||||
|
* to facilitate older implementations using {@link Listener} to migrate to {@link Listener2}.
|
||||||
|
*
|
||||||
* @param error a non-OK status
|
* @param error a non-OK status
|
||||||
* @since 1.21.0
|
* @since 1.21.0
|
||||||
*/
|
*/
|
||||||
|
@ -255,9 +262,14 @@ public abstract class NameResolver {
|
||||||
public abstract void onError(Status error);
|
public abstract void onError(Status error);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Handles updates on resolved addresses and attributes.
|
* Handles updates on resolved addresses and attributes. Must be called from the same
|
||||||
|
* {@link SynchronizationContext} available in {@link NameResolver.Args} that is passed
|
||||||
|
* from the channel.
|
||||||
*
|
*
|
||||||
* @param resolutionResult the resolved server addresses, attributes, and Service Config.
|
* @param resolutionResult the resolved server addresses or error in address resolution,
|
||||||
|
* attributes, and Service Config or error
|
||||||
|
* @return status indicating whether the resolutionResult was accepted by the listener,
|
||||||
|
* typically the result from a load balancer.
|
||||||
* @since 1.66
|
* @since 1.66
|
||||||
*/
|
*/
|
||||||
public Status onResult2(ResolutionResult resolutionResult) {
|
public Status onResult2(ResolutionResult resolutionResult) {
|
||||||
|
|
|
@ -72,6 +72,7 @@ dependencies {
|
||||||
androidTestImplementation testFixtures(project(':grpc-core'))
|
androidTestImplementation testFixtures(project(':grpc-core'))
|
||||||
|
|
||||||
testFixturesImplementation libraries.guava.testlib
|
testFixturesImplementation libraries.guava.testlib
|
||||||
|
testFixturesImplementation testFixtures(project(':grpc-core'))
|
||||||
}
|
}
|
||||||
|
|
||||||
import net.ltgt.gradle.errorprone.CheckSeverity
|
import net.ltgt.gradle.errorprone.CheckSeverity
|
||||||
|
|
|
@ -172,6 +172,12 @@ public final class BinderClientTransportTest {
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@CanIgnoreReturnValue
|
||||||
|
public BinderClientTransportBuilder setPreAuthorizeServer(boolean preAuthorizeServer) {
|
||||||
|
factoryBuilder.setPreAuthorizeServers(preAuthorizeServer);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
public BinderTransport.BinderClientTransport build() {
|
public BinderTransport.BinderClientTransport build() {
|
||||||
return factoryBuilder
|
return factoryBuilder
|
||||||
.buildClientTransportFactory()
|
.buildClientTransportFactory()
|
||||||
|
@ -372,11 +378,12 @@ public final class BinderClientTransportTest {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testBlackHoleSecurityPolicyConnectTimeout() throws Exception {
|
public void testBlackHoleSecurityPolicyAuthTimeout() throws Exception {
|
||||||
SettableAsyncSecurityPolicy securityPolicy = new SettableAsyncSecurityPolicy();
|
SettableAsyncSecurityPolicy securityPolicy = new SettableAsyncSecurityPolicy();
|
||||||
transport =
|
transport =
|
||||||
new BinderClientTransportBuilder()
|
new BinderClientTransportBuilder()
|
||||||
.setSecurityPolicy(securityPolicy)
|
.setSecurityPolicy(securityPolicy)
|
||||||
|
.setPreAuthorizeServer(false)
|
||||||
.setReadyTimeoutMillis(1_234)
|
.setReadyTimeoutMillis(1_234)
|
||||||
.build();
|
.build();
|
||||||
transport.start(transportListener).run();
|
transport.start(transportListener).run();
|
||||||
|
@ -387,15 +394,39 @@ public final class BinderClientTransportTest {
|
||||||
assertThat(transportStatus.getCode()).isEqualTo(Code.DEADLINE_EXCEEDED);
|
assertThat(transportStatus.getCode()).isEqualTo(Code.DEADLINE_EXCEEDED);
|
||||||
assertThat(transportStatus.getDescription()).contains("1234");
|
assertThat(transportStatus.getDescription()).contains("1234");
|
||||||
transportListener.awaitTermination();
|
transportListener.awaitTermination();
|
||||||
|
|
||||||
// If the transport gave up waiting on auth, it should cancel its request.
|
// If the transport gave up waiting on auth, it should cancel its request.
|
||||||
assertThat(authRequest.isCancelled()).isTrue();
|
assertThat(authRequest.isCancelled()).isTrue();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testAsyncSecurityPolicyFailure() throws Exception {
|
public void testBlackHoleSecurityPolicyPreAuthTimeout() throws Exception {
|
||||||
SettableAsyncSecurityPolicy securityPolicy = new SettableAsyncSecurityPolicy();
|
SettableAsyncSecurityPolicy securityPolicy = new SettableAsyncSecurityPolicy();
|
||||||
transport = new BinderClientTransportBuilder().setSecurityPolicy(securityPolicy).build();
|
transport =
|
||||||
|
new BinderClientTransportBuilder()
|
||||||
|
.setSecurityPolicy(securityPolicy)
|
||||||
|
.setPreAuthorizeServer(true)
|
||||||
|
.setReadyTimeoutMillis(1_234)
|
||||||
|
.build();
|
||||||
|
transport.start(transportListener).run();
|
||||||
|
// Take the next authRequest but don't respond to it, in order to trigger the ready timeout.
|
||||||
|
AuthRequest preAuthRequest = securityPolicy.takeNextAuthRequest(TIMEOUT_SECONDS, SECONDS);
|
||||||
|
|
||||||
|
Status transportStatus = transportListener.awaitShutdown();
|
||||||
|
assertThat(transportStatus.getCode()).isEqualTo(Code.DEADLINE_EXCEEDED);
|
||||||
|
assertThat(transportStatus.getDescription()).contains("1234");
|
||||||
|
transportListener.awaitTermination();
|
||||||
|
// If the transport gave up waiting on auth, it should cancel its request.
|
||||||
|
assertThat(preAuthRequest.isCancelled()).isTrue();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testAsyncSecurityPolicyAuthFailure() throws Exception {
|
||||||
|
SettableAsyncSecurityPolicy securityPolicy = new SettableAsyncSecurityPolicy();
|
||||||
|
transport =
|
||||||
|
new BinderClientTransportBuilder()
|
||||||
|
.setPreAuthorizeServer(false)
|
||||||
|
.setSecurityPolicy(securityPolicy)
|
||||||
|
.build();
|
||||||
RuntimeException exception = new NullPointerException();
|
RuntimeException exception = new NullPointerException();
|
||||||
transport.start(transportListener).run();
|
transport.start(transportListener).run();
|
||||||
securityPolicy.takeNextAuthRequest(TIMEOUT_SECONDS, SECONDS).setResult(exception);
|
securityPolicy.takeNextAuthRequest(TIMEOUT_SECONDS, SECONDS).setResult(exception);
|
||||||
|
@ -406,15 +437,55 @@ public final class BinderClientTransportTest {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testAsyncSecurityPolicySuccess() throws Exception {
|
public void testAsyncSecurityPolicyPreAuthFailure() throws Exception {
|
||||||
SettableAsyncSecurityPolicy securityPolicy = new SettableAsyncSecurityPolicy();
|
SettableAsyncSecurityPolicy securityPolicy = new SettableAsyncSecurityPolicy();
|
||||||
transport = new BinderClientTransportBuilder().setSecurityPolicy(securityPolicy).build();
|
transport =
|
||||||
|
new BinderClientTransportBuilder()
|
||||||
|
.setPreAuthorizeServer(true)
|
||||||
|
.setSecurityPolicy(securityPolicy)
|
||||||
|
.build();
|
||||||
|
RuntimeException exception = new NullPointerException();
|
||||||
|
transport.start(transportListener).run();
|
||||||
|
securityPolicy.takeNextAuthRequest(TIMEOUT_SECONDS, SECONDS).setResult(exception);
|
||||||
|
Status transportStatus = transportListener.awaitShutdown();
|
||||||
|
assertThat(transportStatus.getCode()).isEqualTo(Code.INTERNAL);
|
||||||
|
assertThat(transportStatus.getCause()).isEqualTo(exception);
|
||||||
|
transportListener.awaitTermination();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testAsyncSecurityPolicyAuthSuccess() throws Exception {
|
||||||
|
SettableAsyncSecurityPolicy securityPolicy = new SettableAsyncSecurityPolicy();
|
||||||
|
transport =
|
||||||
|
new BinderClientTransportBuilder()
|
||||||
|
.setPreAuthorizeServer(false)
|
||||||
|
.setSecurityPolicy(securityPolicy)
|
||||||
|
.build();
|
||||||
transport.start(transportListener).run();
|
transport.start(transportListener).run();
|
||||||
securityPolicy
|
securityPolicy
|
||||||
.takeNextAuthRequest(TIMEOUT_SECONDS, SECONDS)
|
.takeNextAuthRequest(TIMEOUT_SECONDS, SECONDS)
|
||||||
.setResult(Status.PERMISSION_DENIED);
|
.setResult(Status.PERMISSION_DENIED.withDescription("xyzzy"));
|
||||||
Status transportStatus = transportListener.awaitShutdown();
|
Status transportStatus = transportListener.awaitShutdown();
|
||||||
assertThat(transportStatus.getCode()).isEqualTo(Code.PERMISSION_DENIED);
|
assertThat(transportStatus.getCode()).isEqualTo(Code.PERMISSION_DENIED);
|
||||||
|
assertThat(transportStatus.getDescription()).contains("xyzzy");
|
||||||
|
transportListener.awaitTermination();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testAsyncSecurityPolicyPreAuthSuccess() throws Exception {
|
||||||
|
SettableAsyncSecurityPolicy securityPolicy = new SettableAsyncSecurityPolicy();
|
||||||
|
transport =
|
||||||
|
new BinderClientTransportBuilder()
|
||||||
|
.setPreAuthorizeServer(true)
|
||||||
|
.setSecurityPolicy(securityPolicy)
|
||||||
|
.build();
|
||||||
|
transport.start(transportListener).run();
|
||||||
|
securityPolicy
|
||||||
|
.takeNextAuthRequest(TIMEOUT_SECONDS, SECONDS)
|
||||||
|
.setResult(Status.PERMISSION_DENIED.withDescription("xyzzy"));
|
||||||
|
Status transportStatus = transportListener.awaitShutdown();
|
||||||
|
assertThat(transportStatus.getCode()).isEqualTo(Code.PERMISSION_DENIED);
|
||||||
|
assertThat(transportStatus.getDescription()).contains("xyzzy");
|
||||||
transportListener.awaitTermination();
|
transportListener.awaitTermination();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -18,6 +18,8 @@ package io.grpc.binder;
|
||||||
|
|
||||||
import android.content.Intent;
|
import android.content.Intent;
|
||||||
import android.os.UserHandle;
|
import android.os.UserHandle;
|
||||||
|
import io.grpc.Attributes;
|
||||||
|
import io.grpc.EquivalentAddressGroup;
|
||||||
import io.grpc.ExperimentalApi;
|
import io.grpc.ExperimentalApi;
|
||||||
import io.grpc.NameResolver;
|
import io.grpc.NameResolver;
|
||||||
|
|
||||||
|
@ -46,4 +48,16 @@ public final class ApiConstants {
|
||||||
@ExperimentalApi("https://github.com/grpc/grpc-java/issues/10173")
|
@ExperimentalApi("https://github.com/grpc/grpc-java/issues/10173")
|
||||||
public static final NameResolver.Args.Key<UserHandle> TARGET_ANDROID_USER =
|
public static final NameResolver.Args.Key<UserHandle> TARGET_ANDROID_USER =
|
||||||
NameResolver.Args.Key.create("target-android-user");
|
NameResolver.Args.Key.create("target-android-user");
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Lets you override a Channel's pre-auth configuration (see {@link
|
||||||
|
* BinderChannelBuilder#preAuthorizeServers(boolean)}) for a given {@link EquivalentAddressGroup}.
|
||||||
|
*
|
||||||
|
* <p>A {@link NameResolver} that discovers servers from an untrusted source like PackageManager
|
||||||
|
* can use this to force server pre-auth and prevent abuse.
|
||||||
|
*/
|
||||||
|
@EquivalentAddressGroup.Attr
|
||||||
|
@ExperimentalApi("https://github.com/grpc/grpc-java/issues/12191")
|
||||||
|
public static final Attributes.Key<Boolean> PRE_AUTH_SERVER_OVERRIDE =
|
||||||
|
Attributes.Key.create("pre-auth-server-override");
|
||||||
}
|
}
|
||||||
|
|
|
@ -279,6 +279,35 @@ public final class BinderChannelBuilder extends ForwardingChannelBuilder<BinderC
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks servers against this Channel's {@link SecurityPolicy} *before* binding.
|
||||||
|
*
|
||||||
|
* <p>Android users can be tricked into installing a malicious app with the same package name as a
|
||||||
|
* legitimate server. That's why we don't send calls to a server until it has been authorized by
|
||||||
|
* an appropriate {@link SecurityPolicy}. But merely binding to a malicious server can enable
|
||||||
|
* "keep-alive" and "background activity launch" abuse, even if it's ultimately unauthorized.
|
||||||
|
* Pre-authorization mitigates these threats by performing a preliminary {@link SecurityPolicy}
|
||||||
|
* check against a server app's PackageManager-registered identity without actually creating an
|
||||||
|
* instance of it. This is especially important for security when the server's direct address
|
||||||
|
* isn't known in advance but rather resolved via target URI or discovered by other means.
|
||||||
|
*
|
||||||
|
* <p>Note that, unlike ordinary authorization, pre-authorization is performed against the server
|
||||||
|
* app's UID, not the UID of the process hosting the bound Service. These can be different, most
|
||||||
|
* commonly due to services that set `android:isolatedProcess=true`.
|
||||||
|
*
|
||||||
|
* <p>Pre-authorization is strongly recommended but it remains optional for now because of this
|
||||||
|
* behavior change and the small performance cost.
|
||||||
|
*
|
||||||
|
* <p>The default value of this property is false but it will become true in a future release.
|
||||||
|
* Clients that require a particular behavior should configure it explicitly using this method
|
||||||
|
* rather than relying on the default.
|
||||||
|
*/
|
||||||
|
@ExperimentalApi("https://github.com/grpc/grpc-java/issues/12191")
|
||||||
|
public BinderChannelBuilder preAuthorizeServers(boolean preAuthorize) {
|
||||||
|
transportFactoryBuilder.setPreAuthorizeServers(preAuthorize);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public BinderChannelBuilder idleTimeout(long value, TimeUnit unit) {
|
public BinderChannelBuilder idleTimeout(long value, TimeUnit unit) {
|
||||||
checkState(
|
checkState(
|
||||||
|
|
|
@ -16,10 +16,12 @@
|
||||||
|
|
||||||
package io.grpc.binder.internal;
|
package io.grpc.binder.internal;
|
||||||
|
|
||||||
|
import android.content.pm.ServiceInfo;
|
||||||
import android.os.IBinder;
|
import android.os.IBinder;
|
||||||
import androidx.annotation.AnyThread;
|
import androidx.annotation.AnyThread;
|
||||||
import androidx.annotation.MainThread;
|
import androidx.annotation.MainThread;
|
||||||
import io.grpc.Status;
|
import io.grpc.Status;
|
||||||
|
import io.grpc.StatusException;
|
||||||
|
|
||||||
/** An interface for managing a {@code Binder} connection. */
|
/** An interface for managing a {@code Binder} connection. */
|
||||||
interface Bindable {
|
interface Bindable {
|
||||||
|
@ -45,6 +47,19 @@ interface Bindable {
|
||||||
void onUnbound(Status reason);
|
void onUnbound(Status reason);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetches details about the remote Service from PackageManager without binding to it.
|
||||||
|
*
|
||||||
|
* <p>Resolving an untrusted address before binding to it lets you screen out problematic servers
|
||||||
|
* before giving them a chance to run. However, note that the identity/existence of the resolved
|
||||||
|
* Service can change between the time this method returns and the time you actually bind/connect
|
||||||
|
* to it. For example, suppose the target package gets uninstalled or upgraded right after this
|
||||||
|
* method returns. In {@link Observer#onBound}, you should verify that the server you resolved is
|
||||||
|
* the same one you connected to.
|
||||||
|
*/
|
||||||
|
@AnyThread
|
||||||
|
ServiceInfo resolve() throws StatusException;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Attempt to bind with the remote service.
|
* Attempt to bind with the remote service.
|
||||||
*
|
*
|
||||||
|
|
|
@ -55,6 +55,7 @@ public final class BinderClientTransportFactory implements ClientTransportFactor
|
||||||
final InboundParcelablePolicy inboundParcelablePolicy;
|
final InboundParcelablePolicy inboundParcelablePolicy;
|
||||||
final OneWayBinderProxy.Decorator binderDecorator;
|
final OneWayBinderProxy.Decorator binderDecorator;
|
||||||
final long readyTimeoutMillis;
|
final long readyTimeoutMillis;
|
||||||
|
final boolean preAuthorizeServers; // TODO(jdcormie): Default to true.
|
||||||
|
|
||||||
ScheduledExecutorService executorService;
|
ScheduledExecutorService executorService;
|
||||||
Executor offloadExecutor;
|
Executor offloadExecutor;
|
||||||
|
@ -75,6 +76,7 @@ public final class BinderClientTransportFactory implements ClientTransportFactor
|
||||||
inboundParcelablePolicy = checkNotNull(builder.inboundParcelablePolicy);
|
inboundParcelablePolicy = checkNotNull(builder.inboundParcelablePolicy);
|
||||||
binderDecorator = checkNotNull(builder.binderDecorator);
|
binderDecorator = checkNotNull(builder.binderDecorator);
|
||||||
readyTimeoutMillis = builder.readyTimeoutMillis;
|
readyTimeoutMillis = builder.readyTimeoutMillis;
|
||||||
|
preAuthorizeServers = builder.preAuthorizeServers;
|
||||||
|
|
||||||
executorService = scheduledExecutorPool.getObject();
|
executorService = scheduledExecutorPool.getObject();
|
||||||
offloadExecutor = offloadExecutorPool.getObject();
|
offloadExecutor = offloadExecutorPool.getObject();
|
||||||
|
@ -128,6 +130,7 @@ public final class BinderClientTransportFactory implements ClientTransportFactor
|
||||||
InboundParcelablePolicy inboundParcelablePolicy = InboundParcelablePolicy.DEFAULT;
|
InboundParcelablePolicy inboundParcelablePolicy = InboundParcelablePolicy.DEFAULT;
|
||||||
OneWayBinderProxy.Decorator binderDecorator = OneWayBinderProxy.IDENTITY_DECORATOR;
|
OneWayBinderProxy.Decorator binderDecorator = OneWayBinderProxy.IDENTITY_DECORATOR;
|
||||||
long readyTimeoutMillis = 60_000;
|
long readyTimeoutMillis = 60_000;
|
||||||
|
boolean preAuthorizeServers;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public BinderClientTransportFactory buildClientTransportFactory() {
|
public BinderClientTransportFactory buildClientTransportFactory() {
|
||||||
|
@ -216,5 +219,11 @@ public final class BinderClientTransportFactory implements ClientTransportFactor
|
||||||
this.readyTimeoutMillis = readyTimeoutMillis;
|
this.readyTimeoutMillis = readyTimeoutMillis;
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Whether to check server addresses against the SecurityPolicy *before* binding to them. */
|
||||||
|
public Builder setPreAuthorizeServers(boolean preAuthorizeServers) {
|
||||||
|
this.preAuthorizeServers = preAuthorizeServers;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -19,9 +19,11 @@ 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 static java.util.concurrent.TimeUnit.MILLISECONDS;
|
||||||
|
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
|
import android.content.pm.ServiceInfo;
|
||||||
import android.os.Binder;
|
import android.os.Binder;
|
||||||
import android.os.DeadObjectException;
|
import android.os.DeadObjectException;
|
||||||
import android.os.IBinder;
|
import android.os.IBinder;
|
||||||
|
@ -78,7 +80,6 @@ import java.util.concurrent.Executor;
|
||||||
import java.util.concurrent.ScheduledExecutorService;
|
import java.util.concurrent.ScheduledExecutorService;
|
||||||
import java.util.concurrent.ScheduledFuture;
|
import java.util.concurrent.ScheduledFuture;
|
||||||
import java.util.concurrent.atomic.AtomicInteger;
|
import java.util.concurrent.atomic.AtomicInteger;
|
||||||
import java.util.concurrent.atomic.AtomicLong;
|
|
||||||
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;
|
||||||
|
@ -574,6 +575,7 @@ public abstract class BinderTransport implements IBinder.DeathRecipient {
|
||||||
|
|
||||||
private final long readyTimeoutMillis;
|
private final long readyTimeoutMillis;
|
||||||
private final PingTracker pingTracker;
|
private final PingTracker pingTracker;
|
||||||
|
private final boolean preAuthorizeServer;
|
||||||
|
|
||||||
@Nullable private ManagedClientTransport.Listener clientTransportListener;
|
@Nullable private ManagedClientTransport.Listener clientTransportListener;
|
||||||
|
|
||||||
|
@ -585,6 +587,10 @@ public abstract class BinderTransport implements IBinder.DeathRecipient {
|
||||||
@GuardedBy("this")
|
@GuardedBy("this")
|
||||||
@Nullable private ListenableFuture<Status> authResultFuture; // null before we check auth.
|
@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.
|
* Constructs a new transport instance.
|
||||||
*
|
*
|
||||||
|
@ -609,6 +615,9 @@ public abstract class BinderTransport implements IBinder.DeathRecipient {
|
||||||
this.securityPolicy = factory.securityPolicy;
|
this.securityPolicy = factory.securityPolicy;
|
||||||
this.offloadExecutor = offloadExecutorPool.getObject();
|
this.offloadExecutor = offloadExecutorPool.getObject();
|
||||||
this.readyTimeoutMillis = factory.readyTimeoutMillis;
|
this.readyTimeoutMillis = factory.readyTimeoutMillis;
|
||||||
|
Boolean preAuthServerOverride = options.getEagAttributes().get(PRE_AUTH_SERVER_OVERRIDE);
|
||||||
|
this.preAuthorizeServer =
|
||||||
|
preAuthServerOverride != null ? preAuthServerOverride : factory.preAuthorizeServers;
|
||||||
numInUseStreams = new AtomicInteger();
|
numInUseStreams = new AtomicInteger();
|
||||||
pingTracker = new PingTracker(Ticker.systemTicker(), (id) -> sendPing(id));
|
pingTracker = new PingTracker(Ticker.systemTicker(), (id) -> sendPing(id));
|
||||||
|
|
||||||
|
@ -650,7 +659,16 @@ public abstract class BinderTransport implements IBinder.DeathRecipient {
|
||||||
synchronized (BinderClientTransport.this) {
|
synchronized (BinderClientTransport.this) {
|
||||||
if (inState(TransportState.NOT_STARTED)) {
|
if (inState(TransportState.NOT_STARTED)) {
|
||||||
setState(TransportState.SETUP);
|
setState(TransportState.SETUP);
|
||||||
|
try {
|
||||||
|
if (preAuthorizeServer) {
|
||||||
|
preAuthorize(serviceBinding.resolve());
|
||||||
|
} else {
|
||||||
serviceBinding.bind();
|
serviceBinding.bind();
|
||||||
|
}
|
||||||
|
} catch (StatusException e) {
|
||||||
|
shutdownInternal(e.getStatus(), true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (readyTimeoutMillis >= 0) {
|
if (readyTimeoutMillis >= 0) {
|
||||||
readyTimeoutFuture =
|
readyTimeoutFuture =
|
||||||
getScheduledExecutorService()
|
getScheduledExecutorService()
|
||||||
|
@ -664,6 +682,43 @@ public abstract class BinderTransport implements IBinder.DeathRecipient {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@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() {
|
private synchronized void onReadyTimeout() {
|
||||||
if (inState(TransportState.SETUP)) {
|
if (inState(TransportState.SETUP)) {
|
||||||
readyTimeoutFuture = null;
|
readyTimeoutFuture = null;
|
||||||
|
@ -758,6 +813,9 @@ public abstract class BinderTransport implements IBinder.DeathRecipient {
|
||||||
readyTimeoutFuture.cancel(false);
|
readyTimeoutFuture.cancel(false);
|
||||||
readyTimeoutFuture = null;
|
readyTimeoutFuture = null;
|
||||||
}
|
}
|
||||||
|
if (preAuthResultFuture != null) {
|
||||||
|
preAuthResultFuture.cancel(false); // No effect if already complete.
|
||||||
|
}
|
||||||
if (authResultFuture != null) {
|
if (authResultFuture != null) {
|
||||||
authResultFuture.cancel(false); // No effect if already complete.
|
authResultFuture.cancel(false); // No effect if already complete.
|
||||||
}
|
}
|
||||||
|
@ -780,11 +838,7 @@ public abstract class BinderTransport implements IBinder.DeathRecipient {
|
||||||
shutdownInternal(
|
shutdownInternal(
|
||||||
Status.UNAVAILABLE.withDescription("Malformed SETUP_TRANSPORT data"), true);
|
Status.UNAVAILABLE.withDescription("Malformed SETUP_TRANSPORT data"), true);
|
||||||
} else {
|
} else {
|
||||||
authResultFuture =
|
authResultFuture = checkServerAuthorizationAsync(remoteUid);
|
||||||
(securityPolicy instanceof AsyncSecurityPolicy)
|
|
||||||
? ((AsyncSecurityPolicy) securityPolicy).checkAuthorizationAsync(remoteUid)
|
|
||||||
: Futures.submit(
|
|
||||||
() -> securityPolicy.checkAuthorization(remoteUid), offloadExecutor);
|
|
||||||
Futures.addCallback(
|
Futures.addCallback(
|
||||||
authResultFuture,
|
authResultFuture,
|
||||||
new FutureCallback<Status>() {
|
new FutureCallback<Status>() {
|
||||||
|
@ -803,6 +857,12 @@ public abstract class BinderTransport implements IBinder.DeathRecipient {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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) {
|
private synchronized void handleAuthResult(IBinder binder, Status authorization) {
|
||||||
if (inState(TransportState.SETUP)) {
|
if (inState(TransportState.SETUP)) {
|
||||||
if (!authorization.isOk()) {
|
if (!authorization.isOk()) {
|
||||||
|
|
|
@ -23,14 +23,22 @@ import android.content.ComponentName;
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.content.Intent;
|
import android.content.Intent;
|
||||||
import android.content.ServiceConnection;
|
import android.content.ServiceConnection;
|
||||||
|
import android.content.pm.PackageManager;
|
||||||
|
import android.content.pm.ResolveInfo;
|
||||||
|
import android.content.pm.ServiceInfo;
|
||||||
|
import android.os.Build;
|
||||||
import android.os.IBinder;
|
import android.os.IBinder;
|
||||||
import android.os.UserHandle;
|
import android.os.UserHandle;
|
||||||
import androidx.annotation.AnyThread;
|
import androidx.annotation.AnyThread;
|
||||||
import androidx.annotation.MainThread;
|
import androidx.annotation.MainThread;
|
||||||
import com.google.common.annotations.VisibleForTesting;
|
import com.google.common.annotations.VisibleForTesting;
|
||||||
|
import com.google.common.base.VerifyException;
|
||||||
import com.google.errorprone.annotations.concurrent.GuardedBy;
|
import com.google.errorprone.annotations.concurrent.GuardedBy;
|
||||||
import io.grpc.Status;
|
import io.grpc.Status;
|
||||||
|
import io.grpc.StatusException;
|
||||||
import io.grpc.binder.BinderChannelCredentials;
|
import io.grpc.binder.BinderChannelCredentials;
|
||||||
|
import java.lang.reflect.Method;
|
||||||
|
import java.util.List;
|
||||||
import java.util.concurrent.Executor;
|
import java.util.concurrent.Executor;
|
||||||
import java.util.logging.Level;
|
import java.util.logging.Level;
|
||||||
import java.util.logging.Logger;
|
import java.util.logging.Logger;
|
||||||
|
@ -85,6 +93,8 @@ final class ServiceBinding implements Bindable, ServiceConnection {
|
||||||
private final Observer observer;
|
private final Observer observer;
|
||||||
private final Executor mainThreadExecutor;
|
private final Executor mainThreadExecutor;
|
||||||
|
|
||||||
|
private static volatile Method queryIntentServicesAsUserMethod;
|
||||||
|
|
||||||
@GuardedBy("this")
|
@GuardedBy("this")
|
||||||
private State state;
|
private State state;
|
||||||
|
|
||||||
|
@ -247,6 +257,71 @@ final class ServiceBinding implements Bindable, ServiceConnection {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Sadly the PackageManager#resolveServiceAsUser() API we need isn't part of the SDK or even a
|
||||||
|
// @SystemApi as of this writing. Modern Android prevents even system apps from calling it, by any
|
||||||
|
// means (https://developer.android.com/guide/app-compatibility/restrictions-non-sdk-interfaces).
|
||||||
|
// So instead we call queryIntentServicesAsUser(), which does more than we need but *is* a
|
||||||
|
// @SystemApi in all the SDK versions where we support cross-user Channels.
|
||||||
|
@Nullable
|
||||||
|
private static ResolveInfo resolveServiceAsUser(
|
||||||
|
PackageManager packageManager, Intent intent, int flags, UserHandle targetUserHandle) {
|
||||||
|
List<ResolveInfo> results =
|
||||||
|
queryIntentServicesAsUser(packageManager, intent, flags, targetUserHandle);
|
||||||
|
// The first query result is "what would be returned by resolveService", per the javadoc.
|
||||||
|
return (results != null && !results.isEmpty()) ? results.get(0) : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// The cross-user Channel feature requires the client to be a system app so we assume @SystemApi
|
||||||
|
// queryIntentServicesAsUser() is visible to us at runtime. It would be visible at build time too,
|
||||||
|
// if our host system app were written to call it directly. We only have to use reflection here
|
||||||
|
// because grpc-java is a library built outside the Android source tree where the compiler can't
|
||||||
|
// see the "non-SDK" @SystemApis that we need.
|
||||||
|
@Nullable
|
||||||
|
@SuppressWarnings("unchecked") // Safe by PackageManager#queryIntentServicesAsUser spec in AOSP.
|
||||||
|
private static List<ResolveInfo> queryIntentServicesAsUser(
|
||||||
|
PackageManager packageManager, Intent intent, int flags, UserHandle targetUserHandle) {
|
||||||
|
try {
|
||||||
|
if (queryIntentServicesAsUserMethod == null) {
|
||||||
|
synchronized (ServiceBinding.class) {
|
||||||
|
if (queryIntentServicesAsUserMethod == null) {
|
||||||
|
queryIntentServicesAsUserMethod =
|
||||||
|
PackageManager.class.getMethod(
|
||||||
|
"queryIntentServicesAsUser", Intent.class, int.class, UserHandle.class);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return (List<ResolveInfo>)
|
||||||
|
queryIntentServicesAsUserMethod.invoke(packageManager, intent, flags, targetUserHandle);
|
||||||
|
} catch (ReflectiveOperationException e) {
|
||||||
|
throw new VerifyException(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@AnyThread
|
||||||
|
@Override
|
||||||
|
public ServiceInfo resolve() throws StatusException {
|
||||||
|
checkState(sourceContext != null);
|
||||||
|
PackageManager packageManager = sourceContext.getPackageManager();
|
||||||
|
int flags = 0;
|
||||||
|
if (Build.VERSION.SDK_INT >= 29) {
|
||||||
|
// Filter out non-'directBootAware' <service>s when 'targetUserHandle' is locked. Here's why:
|
||||||
|
// Callers want 'bindIntent' to #resolve() to the same thing a follow-up call to #bind() will.
|
||||||
|
// But bindService() *always* ignores services that can't presently be created for lack of
|
||||||
|
// 'directBootAware'-ness. This flag explicitly tells resolveService() to act the same way.
|
||||||
|
flags |= PackageManager.MATCH_DIRECT_BOOT_AUTO;
|
||||||
|
}
|
||||||
|
ResolveInfo resolveInfo =
|
||||||
|
targetUserHandle != null
|
||||||
|
? resolveServiceAsUser(packageManager, bindIntent, flags, targetUserHandle)
|
||||||
|
: packageManager.resolveService(bindIntent, flags);
|
||||||
|
if (resolveInfo == null) {
|
||||||
|
throw Status.UNIMPLEMENTED // Same status code as when bindService() returns false.
|
||||||
|
.withDescription("resolveService(" + bindIntent + " / " + targetUserHandle + ") was null")
|
||||||
|
.asException();
|
||||||
|
}
|
||||||
|
return resolveInfo.serviceInfo;
|
||||||
|
}
|
||||||
|
|
||||||
@MainThread
|
@MainThread
|
||||||
private void clearReferences() {
|
private void clearReferences() {
|
||||||
sourceContext = null;
|
sourceContext = null;
|
||||||
|
|
|
@ -22,7 +22,13 @@ import static com.google.common.util.concurrent.MoreExecutors.directExecutor;
|
||||||
import static org.robolectric.Shadows.shadowOf;
|
import static org.robolectric.Shadows.shadowOf;
|
||||||
|
|
||||||
import android.app.Application;
|
import android.app.Application;
|
||||||
|
import android.content.pm.ApplicationInfo;
|
||||||
|
import android.content.pm.PackageInfo;
|
||||||
|
import android.content.pm.ServiceInfo;
|
||||||
import androidx.test.core.app.ApplicationProvider;
|
import androidx.test.core.app.ApplicationProvider;
|
||||||
|
import androidx.test.core.content.pm.ApplicationInfoBuilder;
|
||||||
|
import androidx.test.core.content.pm.PackageInfoBuilder;
|
||||||
|
import com.google.common.collect.ImmutableList;
|
||||||
import com.google.common.util.concurrent.Futures;
|
import com.google.common.util.concurrent.Futures;
|
||||||
import com.google.common.util.concurrent.ListenableFuture;
|
import com.google.common.util.concurrent.ListenableFuture;
|
||||||
import com.google.common.util.concurrent.SettableFuture;
|
import com.google.common.util.concurrent.SettableFuture;
|
||||||
|
@ -46,11 +52,13 @@ import org.junit.After;
|
||||||
import org.junit.Before;
|
import org.junit.Before;
|
||||||
import org.junit.Test;
|
import org.junit.Test;
|
||||||
import org.junit.runner.RunWith;
|
import org.junit.runner.RunWith;
|
||||||
import org.robolectric.RobolectricTestRunner;
|
import org.robolectric.ParameterizedRobolectricTestRunner;
|
||||||
|
import org.robolectric.ParameterizedRobolectricTestRunner.Parameter;
|
||||||
|
import org.robolectric.ParameterizedRobolectricTestRunner.Parameters;
|
||||||
import org.robolectric.annotation.LooperMode;
|
import org.robolectric.annotation.LooperMode;
|
||||||
import org.robolectric.annotation.LooperMode.Mode;
|
import org.robolectric.annotation.LooperMode.Mode;
|
||||||
|
|
||||||
@RunWith(RobolectricTestRunner.class)
|
@RunWith(ParameterizedRobolectricTestRunner.class)
|
||||||
@LooperMode(Mode.INSTRUMENTATION_TEST)
|
@LooperMode(Mode.INSTRUMENTATION_TEST)
|
||||||
public final class RobolectricBinderSecurityTest {
|
public final class RobolectricBinderSecurityTest {
|
||||||
|
|
||||||
|
@ -62,10 +70,33 @@ public final class RobolectricBinderSecurityTest {
|
||||||
private ManagedChannel channel;
|
private ManagedChannel channel;
|
||||||
private Server server;
|
private Server server;
|
||||||
|
|
||||||
|
@Parameter public boolean preAuthServersParam;
|
||||||
|
|
||||||
|
@Parameters(name = "preAuthServersParam={0}")
|
||||||
|
public static ImmutableList<Boolean> data() {
|
||||||
|
return ImmutableList.of(true, false);
|
||||||
|
}
|
||||||
|
|
||||||
@Before
|
@Before
|
||||||
public void setUp() {
|
public void setUp() {
|
||||||
|
ApplicationInfo serverAppInfo =
|
||||||
|
ApplicationInfoBuilder.newBuilder().setPackageName(context.getPackageName()).build();
|
||||||
|
serverAppInfo.uid = android.os.Process.myUid();
|
||||||
|
PackageInfo serverPkgInfo =
|
||||||
|
PackageInfoBuilder.newBuilder()
|
||||||
|
.setPackageName(serverAppInfo.packageName)
|
||||||
|
.setApplicationInfo(serverAppInfo)
|
||||||
|
.build();
|
||||||
|
shadowOf(context.getPackageManager()).installPackage(serverPkgInfo);
|
||||||
|
|
||||||
|
ServiceInfo serviceInfo = new ServiceInfo();
|
||||||
|
serviceInfo.name = "SomeService";
|
||||||
|
serviceInfo.packageName = serverAppInfo.packageName;
|
||||||
|
serviceInfo.applicationInfo = serverAppInfo;
|
||||||
|
shadowOf(context.getPackageManager()).addOrUpdateService(serviceInfo);
|
||||||
|
|
||||||
AndroidComponentAddress listenAddress =
|
AndroidComponentAddress listenAddress =
|
||||||
AndroidComponentAddress.forRemoteComponent(context.getPackageName(), "HostService");
|
AndroidComponentAddress.forRemoteComponent(serviceInfo.packageName, serviceInfo.name);
|
||||||
|
|
||||||
MethodDescriptor<Empty, Empty> methodDesc = getMethodDescriptor();
|
MethodDescriptor<Empty, Empty> methodDesc = getMethodDescriptor();
|
||||||
ServerCallHandler<Empty, Empty> callHandler =
|
ServerCallHandler<Empty, Empty> callHandler =
|
||||||
|
@ -110,6 +141,7 @@ public final class RobolectricBinderSecurityTest {
|
||||||
checkNotNull(binderReceiver.get()));
|
checkNotNull(binderReceiver.get()));
|
||||||
channel =
|
channel =
|
||||||
BinderChannelBuilder.forAddress(listenAddress, context)
|
BinderChannelBuilder.forAddress(listenAddress, context)
|
||||||
|
.preAuthorizeServers(preAuthServersParam)
|
||||||
.build();
|
.build();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -16,13 +16,29 @@
|
||||||
|
|
||||||
package io.grpc.binder.internal;
|
package io.grpc.binder.internal;
|
||||||
|
|
||||||
|
import static com.google.common.truth.Truth.assertThat;
|
||||||
|
import static java.util.concurrent.TimeUnit.MILLISECONDS;
|
||||||
|
import static org.mockito.Mockito.never;
|
||||||
|
import static org.mockito.Mockito.timeout;
|
||||||
|
import static org.mockito.Mockito.verify;
|
||||||
import static org.robolectric.Shadows.shadowOf;
|
import static org.robolectric.Shadows.shadowOf;
|
||||||
|
|
||||||
import android.app.Application;
|
import android.app.Application;
|
||||||
import android.content.Intent;
|
import android.content.Intent;
|
||||||
|
import android.content.pm.ApplicationInfo;
|
||||||
|
import android.content.pm.PackageInfo;
|
||||||
|
import android.content.pm.ServiceInfo;
|
||||||
import androidx.test.core.app.ApplicationProvider;
|
import androidx.test.core.app.ApplicationProvider;
|
||||||
|
import androidx.test.core.content.pm.ApplicationInfoBuilder;
|
||||||
|
import androidx.test.core.content.pm.PackageInfoBuilder;
|
||||||
|
import com.google.common.collect.ImmutableList;
|
||||||
|
import io.grpc.Attributes;
|
||||||
import io.grpc.ServerStreamTracer;
|
import io.grpc.ServerStreamTracer;
|
||||||
|
import io.grpc.Status;
|
||||||
import io.grpc.binder.AndroidComponentAddress;
|
import io.grpc.binder.AndroidComponentAddress;
|
||||||
|
import io.grpc.binder.ApiConstants;
|
||||||
|
import io.grpc.binder.AsyncSecurityPolicy;
|
||||||
|
import io.grpc.binder.internal.SettableAsyncSecurityPolicy.AuthRequest;
|
||||||
import io.grpc.internal.AbstractTransportTest;
|
import io.grpc.internal.AbstractTransportTest;
|
||||||
import io.grpc.internal.ClientTransportFactory.ClientTransportOptions;
|
import io.grpc.internal.ClientTransportFactory.ClientTransportOptions;
|
||||||
import io.grpc.internal.GrpcUtil;
|
import io.grpc.internal.GrpcUtil;
|
||||||
|
@ -33,12 +49,20 @@ import io.grpc.internal.SharedResourcePool;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.concurrent.Executor;
|
import java.util.concurrent.Executor;
|
||||||
import java.util.concurrent.ScheduledExecutorService;
|
import java.util.concurrent.ScheduledExecutorService;
|
||||||
|
import org.junit.Before;
|
||||||
import org.junit.Ignore;
|
import org.junit.Ignore;
|
||||||
|
import org.junit.Rule;
|
||||||
import org.junit.Test;
|
import org.junit.Test;
|
||||||
import org.junit.runner.RunWith;
|
import org.junit.runner.RunWith;
|
||||||
import org.robolectric.RobolectricTestRunner;
|
import org.mockito.Mock;
|
||||||
|
import org.mockito.junit.MockitoJUnit;
|
||||||
|
import org.mockito.junit.MockitoRule;
|
||||||
|
import org.robolectric.ParameterizedRobolectricTestRunner;
|
||||||
|
import org.robolectric.ParameterizedRobolectricTestRunner.Parameter;
|
||||||
|
import org.robolectric.ParameterizedRobolectricTestRunner.Parameters;
|
||||||
import org.robolectric.annotation.LooperMode;
|
import org.robolectric.annotation.LooperMode;
|
||||||
import org.robolectric.annotation.LooperMode.Mode;
|
import org.robolectric.annotation.LooperMode.Mode;
|
||||||
|
import org.robolectric.shadows.ShadowBinder;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* All of the AbstractTransportTest cases applied to {@link BinderTransport} running in a
|
* All of the AbstractTransportTest cases applied to {@link BinderTransport} running in a
|
||||||
|
@ -52,7 +76,7 @@ import org.robolectric.annotation.LooperMode.Mode;
|
||||||
* meaning test cases don't run on the main thread. This supports the AbstractTransportTest approach
|
* meaning test cases don't run on the main thread. This supports the AbstractTransportTest approach
|
||||||
* where the test thread frequently blocks waiting for transport state changes to take effect.
|
* where the test thread frequently blocks waiting for transport state changes to take effect.
|
||||||
*/
|
*/
|
||||||
@RunWith(RobolectricTestRunner.class)
|
@RunWith(ParameterizedRobolectricTestRunner.class)
|
||||||
@LooperMode(Mode.INSTRUMENTATION_TEST)
|
@LooperMode(Mode.INSTRUMENTATION_TEST)
|
||||||
public final class RobolectricBinderTransportTest extends AbstractTransportTest {
|
public final class RobolectricBinderTransportTest extends AbstractTransportTest {
|
||||||
|
|
||||||
|
@ -64,13 +88,56 @@ public final class RobolectricBinderTransportTest extends AbstractTransportTest
|
||||||
private final ObjectPool<Executor> serverExecutorPool =
|
private final ObjectPool<Executor> serverExecutorPool =
|
||||||
SharedResourcePool.forResource(GrpcUtil.SHARED_CHANNEL_EXECUTOR);
|
SharedResourcePool.forResource(GrpcUtil.SHARED_CHANNEL_EXECUTOR);
|
||||||
|
|
||||||
|
@Rule public MockitoRule mocks = MockitoJUnit.rule();
|
||||||
|
|
||||||
|
@Mock AsyncSecurityPolicy mockClientSecurityPolicy;
|
||||||
|
|
||||||
|
ApplicationInfo serverAppInfo;
|
||||||
|
PackageInfo serverPkgInfo;
|
||||||
|
ServiceInfo serviceInfo;
|
||||||
|
|
||||||
private int nextServerAddress;
|
private int nextServerAddress;
|
||||||
|
|
||||||
|
@Parameter public boolean preAuthServersParam;
|
||||||
|
|
||||||
|
@Parameters(name = "preAuthServersParam={0}")
|
||||||
|
public static ImmutableList<Boolean> data() {
|
||||||
|
return ImmutableList.of(true, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setUp() {
|
||||||
|
serverAppInfo =
|
||||||
|
ApplicationInfoBuilder.newBuilder().setPackageName("the.server.package").build();
|
||||||
|
serverAppInfo.uid = android.os.Process.myUid();
|
||||||
|
serverPkgInfo =
|
||||||
|
PackageInfoBuilder.newBuilder()
|
||||||
|
.setPackageName(serverAppInfo.packageName)
|
||||||
|
.setApplicationInfo(serverAppInfo)
|
||||||
|
.build();
|
||||||
|
shadowOf(application.getPackageManager()).installPackage(serverPkgInfo);
|
||||||
|
|
||||||
|
serviceInfo = new ServiceInfo();
|
||||||
|
serviceInfo.name = "SomeService";
|
||||||
|
serviceInfo.packageName = serverAppInfo.packageName;
|
||||||
|
serviceInfo.applicationInfo = serverAppInfo;
|
||||||
|
shadowOf(application.getPackageManager()).addOrUpdateService(serviceInfo);
|
||||||
|
|
||||||
|
super.setUp();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Before
|
||||||
|
public void requestRealisticBindServiceBehavior() {
|
||||||
|
shadowOf(application).setBindServiceCallsOnServiceConnectedDirectly(false);
|
||||||
|
shadowOf(application).setUnbindServiceCallsOnServiceDisconnected(false);
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected InternalServer newServer(List<ServerStreamTracer.Factory> streamTracerFactories) {
|
protected InternalServer newServer(List<ServerStreamTracer.Factory> streamTracerFactories) {
|
||||||
AndroidComponentAddress listenAddr = AndroidComponentAddress.forBindIntent(
|
AndroidComponentAddress listenAddr =
|
||||||
|
AndroidComponentAddress.forBindIntent(
|
||||||
new Intent()
|
new Intent()
|
||||||
.setClassName(application.getPackageName(), "HostService")
|
.setClassName(serviceInfo.packageName, serviceInfo.name)
|
||||||
.setAction("io.grpc.action.BIND." + nextServerAddress++));
|
.setAction("io.grpc.action.BIND." + nextServerAddress++));
|
||||||
|
|
||||||
BinderServer binderServer =
|
BinderServer binderServer =
|
||||||
|
@ -81,6 +148,7 @@ public final class RobolectricBinderTransportTest extends AbstractTransportTest
|
||||||
.setStreamTracerFactories(streamTracerFactories)
|
.setStreamTracerFactories(streamTracerFactories)
|
||||||
.build();
|
.build();
|
||||||
|
|
||||||
|
shadowOf(application.getPackageManager()).addServiceIfNotPresent(listenAddr.getComponent());
|
||||||
shadowOf(application)
|
shadowOf(application)
|
||||||
.setComponentNameAndServiceForBindServiceForIntent(
|
.setComponentNameAndServiceForBindServiceForIntent(
|
||||||
listenAddr.asBindIntent(), listenAddr.getComponent(), binderServer.getHostBinder());
|
listenAddr.asBindIntent(), listenAddr.getComponent(), binderServer.getHostBinder());
|
||||||
|
@ -97,22 +165,30 @@ public final class RobolectricBinderTransportTest extends AbstractTransportTest
|
||||||
return newServer(streamTracerFactories);
|
return newServer(streamTracerFactories);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
BinderClientTransportFactory.Builder newClientTransportFactoryBuilder() {
|
||||||
protected ManagedClientTransport newClientTransport(InternalServer server) {
|
return new BinderClientTransportFactory.Builder()
|
||||||
BinderClientTransportFactory.Builder builder =
|
.setPreAuthorizeServers(preAuthServersParam)
|
||||||
new BinderClientTransportFactory.Builder()
|
|
||||||
.setSourceContext(application)
|
.setSourceContext(application)
|
||||||
.setScheduledExecutorPool(executorServicePool)
|
.setScheduledExecutorPool(executorServicePool)
|
||||||
.setOffloadExecutorPool(offloadExecutorPool);
|
.setOffloadExecutorPool(offloadExecutorPool);
|
||||||
|
}
|
||||||
|
|
||||||
|
BinderClientTransportBuilder newClientTransportBuilder() {
|
||||||
|
return new BinderClientTransportBuilder()
|
||||||
|
.setFactory(newClientTransportFactoryBuilder().buildClientTransportFactory())
|
||||||
|
.setServerAddress(server.getListenSocketAddress());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected ManagedClientTransport newClientTransport(InternalServer server) {
|
||||||
ClientTransportOptions options = new ClientTransportOptions();
|
ClientTransportOptions options = new ClientTransportOptions();
|
||||||
options.setEagAttributes(eagAttrs());
|
options.setEagAttributes(eagAttrs());
|
||||||
options.setChannelLogger(transportLogger());
|
options.setChannelLogger(transportLogger());
|
||||||
|
|
||||||
return new BinderTransport.BinderClientTransport(
|
return newClientTransportBuilder()
|
||||||
builder.buildClientTransportFactory(),
|
.setServerAddress(server.getListenSocketAddress())
|
||||||
(AndroidComponentAddress) server.getListenSocketAddress(),
|
.setOptions(options)
|
||||||
options);
|
.build();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -120,6 +196,74 @@ public final class RobolectricBinderTransportTest extends AbstractTransportTest
|
||||||
return ((AndroidComponentAddress) server.getListenSocketAddress()).getAuthority();
|
return ((AndroidComponentAddress) server.getListenSocketAddress()).getAuthority();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void clientAuthorizesServerUidsInOrder() throws Exception {
|
||||||
|
// TODO(jdcormie): In real Android, Binder#getCallingUid is thread-local but Robolectric only
|
||||||
|
// lets us fake value this *globally*. So the ShadowBinder#setCallingUid() here unrealistically
|
||||||
|
// affects the server's view of the client's uid too. For now this doesn't matter because this
|
||||||
|
// test never exercises server SecurityPolicy.
|
||||||
|
ShadowBinder.setCallingUid(11111); // UID of the server *process*.
|
||||||
|
|
||||||
|
serverPkgInfo.applicationInfo.uid = 22222; // UID of the server *app*, which can be different.
|
||||||
|
shadowOf(application.getPackageManager()).installPackage(serverPkgInfo);
|
||||||
|
shadowOf(application.getPackageManager()).addOrUpdateService(serviceInfo);
|
||||||
|
server = newServer(ImmutableList.of());
|
||||||
|
server.start(serverListener);
|
||||||
|
|
||||||
|
SettableAsyncSecurityPolicy securityPolicy = new SettableAsyncSecurityPolicy();
|
||||||
|
client =
|
||||||
|
newClientTransportBuilder()
|
||||||
|
.setFactory(
|
||||||
|
newClientTransportFactoryBuilder()
|
||||||
|
.setSecurityPolicy(securityPolicy)
|
||||||
|
.buildClientTransportFactory())
|
||||||
|
.build();
|
||||||
|
runIfNotNull(client.start(mockClientTransportListener));
|
||||||
|
|
||||||
|
if (preAuthServersParam) {
|
||||||
|
AuthRequest preAuthRequest = securityPolicy.takeNextAuthRequest(TIMEOUT_MS, MILLISECONDS);
|
||||||
|
assertThat(preAuthRequest.uid).isEqualTo(22222);
|
||||||
|
verify(mockClientTransportListener, never()).transportReady();
|
||||||
|
preAuthRequest.setResult(Status.OK);
|
||||||
|
}
|
||||||
|
|
||||||
|
AuthRequest authRequest = securityPolicy.takeNextAuthRequest(TIMEOUT_MS, MILLISECONDS);
|
||||||
|
assertThat(authRequest.uid).isEqualTo(11111);
|
||||||
|
verify(mockClientTransportListener, never()).transportReady();
|
||||||
|
authRequest.setResult(Status.OK);
|
||||||
|
|
||||||
|
verify(mockClientTransportListener, timeout(TIMEOUT_MS)).transportReady();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void eagAttributeCanOverrideChannelPreAuthServerSetting() throws Exception {
|
||||||
|
server.start(serverListener);
|
||||||
|
SettableAsyncSecurityPolicy securityPolicy = new SettableAsyncSecurityPolicy();
|
||||||
|
ClientTransportOptions options = new ClientTransportOptions();
|
||||||
|
options.setEagAttributes(
|
||||||
|
Attributes.newBuilder().set(ApiConstants.PRE_AUTH_SERVER_OVERRIDE, true).build());
|
||||||
|
client =
|
||||||
|
newClientTransportBuilder()
|
||||||
|
.setOptions(options)
|
||||||
|
.setFactory(
|
||||||
|
newClientTransportFactoryBuilder()
|
||||||
|
.setPreAuthorizeServers(preAuthServersParam) // To be overridden.
|
||||||
|
.setSecurityPolicy(securityPolicy)
|
||||||
|
.buildClientTransportFactory())
|
||||||
|
.build();
|
||||||
|
runIfNotNull(client.start(mockClientTransportListener));
|
||||||
|
|
||||||
|
AuthRequest preAuthRequest = securityPolicy.takeNextAuthRequest(TIMEOUT_MS, MILLISECONDS);
|
||||||
|
verify(mockClientTransportListener, never()).transportReady();
|
||||||
|
preAuthRequest.setResult(Status.OK);
|
||||||
|
|
||||||
|
AuthRequest authRequest = securityPolicy.takeNextAuthRequest(TIMEOUT_MS, MILLISECONDS);
|
||||||
|
verify(mockClientTransportListener, never()).transportReady();
|
||||||
|
authRequest.setResult(Status.OK);
|
||||||
|
|
||||||
|
verify(mockClientTransportListener, timeout(TIMEOUT_MS)).transportReady();
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@Ignore("See BinderTransportTest#socketStats.")
|
@Ignore("See BinderTransportTest#socketStats.")
|
||||||
@Override
|
@Override
|
||||||
|
|
|
@ -19,6 +19,7 @@ package io.grpc.binder.internal;
|
||||||
import static android.content.Context.BIND_AUTO_CREATE;
|
import static android.content.Context.BIND_AUTO_CREATE;
|
||||||
import static android.os.Looper.getMainLooper;
|
import static android.os.Looper.getMainLooper;
|
||||||
import static com.google.common.truth.Truth.assertThat;
|
import static com.google.common.truth.Truth.assertThat;
|
||||||
|
import static org.junit.Assert.assertThrows;
|
||||||
import static org.junit.Assert.fail;
|
import static org.junit.Assert.fail;
|
||||||
import static org.robolectric.Shadows.shadowOf;
|
import static org.robolectric.Shadows.shadowOf;
|
||||||
|
|
||||||
|
@ -27,6 +28,7 @@ import android.app.admin.DevicePolicyManager;
|
||||||
import android.content.ComponentName;
|
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.os.IBinder;
|
import android.os.IBinder;
|
||||||
import android.os.Parcel;
|
import android.os.Parcel;
|
||||||
import android.os.UserHandle;
|
import android.os.UserHandle;
|
||||||
|
@ -34,6 +36,7 @@ import androidx.core.content.ContextCompat;
|
||||||
import androidx.test.core.app.ApplicationProvider;
|
import androidx.test.core.app.ApplicationProvider;
|
||||||
import io.grpc.Status;
|
import io.grpc.Status;
|
||||||
import io.grpc.Status.Code;
|
import io.grpc.Status.Code;
|
||||||
|
import io.grpc.StatusException;
|
||||||
import io.grpc.binder.BinderChannelCredentials;
|
import io.grpc.binder.BinderChannelCredentials;
|
||||||
import io.grpc.binder.internal.Bindable.Observer;
|
import io.grpc.binder.internal.Bindable.Observer;
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
|
@ -59,6 +62,7 @@ public final class ServiceBindingTest {
|
||||||
|
|
||||||
private Application appContext;
|
private Application appContext;
|
||||||
private ComponentName serviceComponent;
|
private ComponentName serviceComponent;
|
||||||
|
private ServiceInfo serviceInfo = new ServiceInfo();
|
||||||
private ShadowApplication shadowApplication;
|
private ShadowApplication shadowApplication;
|
||||||
private TestObserver observer;
|
private TestObserver observer;
|
||||||
private ServiceBinding binding;
|
private ServiceBinding binding;
|
||||||
|
@ -67,13 +71,17 @@ public final class ServiceBindingTest {
|
||||||
public void setUp() {
|
public void setUp() {
|
||||||
appContext = ApplicationProvider.getApplicationContext();
|
appContext = ApplicationProvider.getApplicationContext();
|
||||||
serviceComponent = new ComponentName("DUMMY", "SERVICE");
|
serviceComponent = new ComponentName("DUMMY", "SERVICE");
|
||||||
|
serviceInfo.packageName = serviceComponent.getPackageName();
|
||||||
|
serviceInfo.name = serviceComponent.getClassName();
|
||||||
observer = new TestObserver();
|
observer = new TestObserver();
|
||||||
|
|
||||||
shadowApplication = shadowOf(appContext);
|
shadowApplication = shadowOf(appContext);
|
||||||
shadowApplication.setComponentNameAndServiceForBindService(serviceComponent, mockBinder);
|
shadowApplication.setComponentNameAndServiceForBindService(serviceComponent, mockBinder);
|
||||||
|
shadowOf(appContext.getPackageManager()).addOrUpdateService(serviceInfo);
|
||||||
|
|
||||||
// Don't call onServiceDisconnected() upon unbindService(), just like the real Android doesn't.
|
// Don't call onServiceDisconnected() upon unbindService(), just like the real Android doesn't.
|
||||||
shadowApplication.setUnbindServiceCallsOnServiceDisconnected(false);
|
shadowApplication.setUnbindServiceCallsOnServiceDisconnected(false);
|
||||||
|
shadowApplication.setBindServiceCallsOnServiceConnectedDirectly(false);
|
||||||
|
|
||||||
binding = newBuilder().build();
|
binding = newBuilder().build();
|
||||||
shadowOf(getMainLooper()).idle();
|
shadowOf(getMainLooper()).idle();
|
||||||
|
@ -276,6 +284,49 @@ public final class ServiceBindingTest {
|
||||||
assertThat(binding.isSourceContextCleared()).isFalse();
|
assertThat(binding.isSourceContextCleared()).isFalse();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testResolve() throws Exception {
|
||||||
|
serviceInfo.processName = "x"; // ServiceInfo has no equals() so look for one distinctive field.
|
||||||
|
shadowOf(appContext.getPackageManager()).addOrUpdateService(serviceInfo);
|
||||||
|
ServiceInfo resolvedServiceInfo = binding.resolve();
|
||||||
|
assertThat(resolvedServiceInfo.processName).isEqualTo(serviceInfo.processName);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@Config(sdk = 33)
|
||||||
|
public void testResolveWithTargetUserHandle() throws Exception {
|
||||||
|
serviceInfo.processName = "x"; // ServiceInfo has no equals() so look for one distinctive field.
|
||||||
|
// Robolectric just ignores the user arg to resolveServiceAsUser() so this is all we can do.
|
||||||
|
shadowOf(appContext.getPackageManager()).addOrUpdateService(serviceInfo);
|
||||||
|
binding = newBuilder().setTargetUserHandle(generateUserHandle(/* userId= */ 0)).build();
|
||||||
|
ServiceInfo resolvedServiceInfo = binding.resolve();
|
||||||
|
assertThat(resolvedServiceInfo.processName).isEqualTo(serviceInfo.processName);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testResolveNonExistentServiceThrows() throws Exception {
|
||||||
|
ComponentName doesNotExistService = new ComponentName("does.not.exist", "NoService");
|
||||||
|
binding = newBuilder().setTargetComponent(doesNotExistService).build();
|
||||||
|
StatusException statusException = assertThrows(StatusException.class, binding::resolve);
|
||||||
|
assertThat(statusException.getStatus().getCode()).isEqualTo(Code.UNIMPLEMENTED);
|
||||||
|
assertThat(statusException.getStatus().getDescription()).contains("does.not.exist");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@Config(sdk = 33)
|
||||||
|
public void testResolveNonExistentServiceWithTargetUserThrows() throws Exception {
|
||||||
|
ComponentName doesNotExistService = new ComponentName("does.not.exist", "NoService");
|
||||||
|
binding =
|
||||||
|
newBuilder()
|
||||||
|
.setTargetUserHandle(generateUserHandle(/* userId= */ 12345))
|
||||||
|
.setTargetComponent(doesNotExistService)
|
||||||
|
.build();
|
||||||
|
StatusException statusException = assertThrows(StatusException.class, binding::resolve);
|
||||||
|
assertThat(statusException.getStatus().getCode()).isEqualTo(Code.UNIMPLEMENTED);
|
||||||
|
assertThat(statusException.getStatus().getDescription()).contains("does.not.exist");
|
||||||
|
assertThat(statusException.getStatus().getDescription()).contains("12345");
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@Config(sdk = 30)
|
@Config(sdk = 30)
|
||||||
public void testBindWithDeviceAdmin() throws Exception {
|
public void testBindWithDeviceAdmin() throws Exception {
|
||||||
|
@ -284,7 +335,7 @@ public final class ServiceBindingTest {
|
||||||
allowBindDeviceAdminForUser(appContext, adminComponent, /* userId= */ 0);
|
allowBindDeviceAdminForUser(appContext, adminComponent, /* userId= */ 0);
|
||||||
binding =
|
binding =
|
||||||
newBuilder()
|
newBuilder()
|
||||||
.setTargetUserHandle(UserHandle.getUserHandleForUid(/* userId= */ 0))
|
.setTargetUserHandle(UserHandle.getUserHandleForUid(/* uid= */ 0))
|
||||||
.setTargetUserHandle(generateUserHandle(/* userId= */ 0))
|
.setTargetUserHandle(generateUserHandle(/* userId= */ 0))
|
||||||
.setChannelCredentials(BinderChannelCredentials.forDevicePolicyAdmin(adminComponent))
|
.setChannelCredentials(BinderChannelCredentials.forDevicePolicyAdmin(adminComponent))
|
||||||
.build();
|
.build();
|
||||||
|
|
|
@ -0,0 +1,61 @@
|
||||||
|
/*
|
||||||
|
* 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 io.grpc.ChannelLogger;
|
||||||
|
import io.grpc.internal.ClientTransportFactory.ClientTransportOptions;
|
||||||
|
import io.grpc.internal.TestUtils.NoopChannelLogger;
|
||||||
|
import java.net.SocketAddress;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helps unit tests create {@link BinderTransport.BinderClientTransport} instances without having to
|
||||||
|
* mention irrelevant details (go/tott/719).
|
||||||
|
*/
|
||||||
|
public class BinderClientTransportBuilder {
|
||||||
|
private BinderClientTransportFactory factory;
|
||||||
|
private SocketAddress serverAddress;
|
||||||
|
private ChannelLogger channelLogger = new NoopChannelLogger();
|
||||||
|
private io.grpc.internal.ClientTransportFactory.ClientTransportOptions options =
|
||||||
|
new ClientTransportOptions();
|
||||||
|
|
||||||
|
public BinderClientTransportBuilder setServerAddress(SocketAddress serverAddress) {
|
||||||
|
this.serverAddress = checkNotNull(serverAddress);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public BinderClientTransportBuilder setChannelLogger(ChannelLogger channelLogger) {
|
||||||
|
this.channelLogger = checkNotNull(channelLogger);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public BinderClientTransportBuilder setOptions(ClientTransportOptions options) {
|
||||||
|
this.options = checkNotNull(options);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public BinderClientTransportBuilder setFactory(BinderClientTransportFactory factory) {
|
||||||
|
this.factory = checkNotNull(factory);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public BinderTransport.BinderClientTransport build() {
|
||||||
|
return factory.newClientTransport(
|
||||||
|
checkNotNull(serverAddress), checkNotNull(options), checkNotNull(channelLogger));
|
||||||
|
}
|
||||||
|
}
|
|
@ -92,7 +92,7 @@ public abstract class AbstractTransportTest {
|
||||||
*/
|
*/
|
||||||
public static final int TEST_FLOW_CONTROL_WINDOW = 65 * 1024;
|
public static final int TEST_FLOW_CONTROL_WINDOW = 65 * 1024;
|
||||||
|
|
||||||
private static final int TIMEOUT_MS = 5000;
|
protected static final int TIMEOUT_MS = 5000;
|
||||||
|
|
||||||
protected static final String GRPC_EXPERIMENTAL_SUPPORT_TRACING_MESSAGE_SIZES =
|
protected static final String GRPC_EXPERIMENTAL_SUPPORT_TRACING_MESSAGE_SIZES =
|
||||||
"GRPC_EXPERIMENTAL_SUPPORT_TRACING_MESSAGE_SIZES";
|
"GRPC_EXPERIMENTAL_SUPPORT_TRACING_MESSAGE_SIZES";
|
||||||
|
@ -2163,7 +2163,7 @@ public abstract class AbstractTransportTest {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void runIfNotNull(Runnable runnable) {
|
protected static void runIfNotNull(Runnable runnable) {
|
||||||
if (runnable != null) {
|
if (runnable != null) {
|
||||||
runnable.run();
|
runnable.run();
|
||||||
}
|
}
|
||||||
|
|
|
@ -101,10 +101,10 @@ def grpc_java_repositories(bzlmod = False):
|
||||||
if not native.existing_rule("com_github_cncf_xds"):
|
if not native.existing_rule("com_github_cncf_xds"):
|
||||||
http_archive(
|
http_archive(
|
||||||
name = "com_github_cncf_xds",
|
name = "com_github_cncf_xds",
|
||||||
strip_prefix = "xds-024c85f92f20cab567a83acc50934c7f9711d124",
|
strip_prefix = "xds-2ac532fd44436293585084f8d94c6bdb17835af0",
|
||||||
sha256 = "5f403aa681711500ca8e62387be3e37d971977db6e88616fc21862a406430649",
|
sha256 = "790c4c83b6950bb602fec221f6a529d9f368cdc8852aae7d2592d0d04b015f37",
|
||||||
urls = [
|
urls = [
|
||||||
"https://github.com/cncf/xds/archive/024c85f92f20cab567a83acc50934c7f9711d124.tar.gz",
|
"https://github.com/cncf/xds/archive/2ac532fd44436293585084f8d94c6bdb17835af0.tar.gz",
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
if not bzlmod and not native.existing_rule("com_github_grpc_grpc"):
|
if not bzlmod and not native.existing_rule("com_github_grpc_grpc"):
|
||||||
|
|
|
@ -89,7 +89,7 @@ public final class ClusterResolverLoadBalancerProvider extends LoadBalancerProvi
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public int hashCode() {
|
public int hashCode() {
|
||||||
return Objects.hash(discoveryMechanisms, lbConfig);
|
return Objects.hash(discoveryMechanisms, lbConfig, isHttp11ProxyAvailable);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -102,7 +102,8 @@ public final class ClusterResolverLoadBalancerProvider extends LoadBalancerProvi
|
||||||
}
|
}
|
||||||
ClusterResolverConfig that = (ClusterResolverConfig) o;
|
ClusterResolverConfig that = (ClusterResolverConfig) o;
|
||||||
return discoveryMechanisms.equals(that.discoveryMechanisms)
|
return discoveryMechanisms.equals(that.discoveryMechanisms)
|
||||||
&& lbConfig.equals(that.lbConfig);
|
&& lbConfig.equals(that.lbConfig)
|
||||||
|
&& isHttp11ProxyAvailable == that.isHttp11ProxyAvailable;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -110,6 +111,7 @@ public final class ClusterResolverLoadBalancerProvider extends LoadBalancerProvi
|
||||||
return MoreObjects.toStringHelper(this)
|
return MoreObjects.toStringHelper(this)
|
||||||
.add("discoveryMechanisms", discoveryMechanisms)
|
.add("discoveryMechanisms", discoveryMechanisms)
|
||||||
.add("lbConfig", lbConfig)
|
.add("lbConfig", lbConfig)
|
||||||
|
.add("isHttp11ProxyAvailable", isHttp11ProxyAvailable)
|
||||||
.toString();
|
.toString();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -51,6 +51,7 @@ import java.util.HashMap;
|
||||||
import java.util.HashSet;
|
import java.util.HashSet;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
import java.util.Objects;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
import javax.annotation.Nullable;
|
import javax.annotation.Nullable;
|
||||||
|
@ -523,6 +524,22 @@ final class RingHashLoadBalancer extends MultiChildLoadBalancer {
|
||||||
this.requestHashHeader = requestHashHeader;
|
this.requestHashHeader = requestHashHeader;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean equals(Object o) {
|
||||||
|
if (!(o instanceof RingHashConfig)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
RingHashConfig that = (RingHashConfig) o;
|
||||||
|
return this.minRingSize == that.minRingSize
|
||||||
|
&& this.maxRingSize == that.maxRingSize
|
||||||
|
&& Objects.equals(this.requestHashHeader, that.requestHashHeader);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int hashCode() {
|
||||||
|
return Objects.hash(minRingSize, maxRingSize, requestHashHeader);
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public String toString() {
|
public String toString() {
|
||||||
return MoreObjects.toStringHelper(this)
|
return MoreObjects.toStringHelper(this)
|
||||||
|
|
|
@ -32,6 +32,7 @@ import static org.mockito.Mockito.when;
|
||||||
|
|
||||||
import com.google.common.collect.ImmutableMap;
|
import com.google.common.collect.ImmutableMap;
|
||||||
import com.google.common.collect.Iterables;
|
import com.google.common.collect.Iterables;
|
||||||
|
import com.google.common.testing.EqualsTester;
|
||||||
import io.grpc.Attributes;
|
import io.grpc.Attributes;
|
||||||
import io.grpc.ChannelLogger;
|
import io.grpc.ChannelLogger;
|
||||||
import io.grpc.ConnectivityState;
|
import io.grpc.ConnectivityState;
|
||||||
|
@ -1199,6 +1200,27 @@ public class ClusterResolverLoadBalancerTest {
|
||||||
any(ConnectivityState.class), any(SubchannelPicker.class));
|
any(ConnectivityState.class), any(SubchannelPicker.class));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void config_equalsTester() {
|
||||||
|
new EqualsTester()
|
||||||
|
.addEqualityGroup(
|
||||||
|
new ClusterResolverConfig(
|
||||||
|
Collections.singletonList(edsDiscoveryMechanism1), leastRequest, false),
|
||||||
|
new ClusterResolverConfig(
|
||||||
|
Collections.singletonList(edsDiscoveryMechanism1), leastRequest, false))
|
||||||
|
.addEqualityGroup(new ClusterResolverConfig(
|
||||||
|
Collections.singletonList(edsDiscoveryMechanism1), roundRobin, false))
|
||||||
|
.addEqualityGroup(new ClusterResolverConfig(
|
||||||
|
Collections.singletonList(edsDiscoveryMechanism1), leastRequest, true))
|
||||||
|
.addEqualityGroup(new ClusterResolverConfig(
|
||||||
|
Collections.singletonList(edsDiscoveryMechanismWithOutlierDetection),
|
||||||
|
leastRequest,
|
||||||
|
false))
|
||||||
|
.addEqualityGroup(new ClusterResolverConfig(
|
||||||
|
Arrays.asList(edsDiscoveryMechanism1, edsDiscoveryMechanism2), leastRequest, false))
|
||||||
|
.testEquals();
|
||||||
|
}
|
||||||
|
|
||||||
private void deliverLbConfig(ClusterResolverConfig config) {
|
private void deliverLbConfig(ClusterResolverConfig config) {
|
||||||
loadBalancer.acceptResolvedAddresses(
|
loadBalancer.acceptResolvedAddresses(
|
||||||
ResolvedAddresses.newBuilder()
|
ResolvedAddresses.newBuilder()
|
||||||
|
|
|
@ -42,6 +42,7 @@ import static org.mockito.Mockito.verifyNoMoreInteractions;
|
||||||
|
|
||||||
import com.google.common.collect.Iterables;
|
import com.google.common.collect.Iterables;
|
||||||
import com.google.common.primitives.UnsignedInteger;
|
import com.google.common.primitives.UnsignedInteger;
|
||||||
|
import com.google.common.testing.EqualsTester;
|
||||||
import io.grpc.Attributes;
|
import io.grpc.Attributes;
|
||||||
import io.grpc.CallOptions;
|
import io.grpc.CallOptions;
|
||||||
import io.grpc.ConnectivityState;
|
import io.grpc.ConnectivityState;
|
||||||
|
@ -1113,6 +1114,19 @@ public class RingHashLoadBalancerTest {
|
||||||
assertThat(picks).containsExactly(subchannel1);
|
assertThat(picks).containsExactly(subchannel1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void config_equalsTester() {
|
||||||
|
new EqualsTester()
|
||||||
|
.addEqualityGroup(
|
||||||
|
new RingHashConfig(1, 2, "headerA"),
|
||||||
|
new RingHashConfig(1, 2, "headerA"))
|
||||||
|
.addEqualityGroup(new RingHashConfig(1, 1, "headerA"))
|
||||||
|
.addEqualityGroup(new RingHashConfig(2, 2, "headerA"))
|
||||||
|
.addEqualityGroup(new RingHashConfig(1, 2, "headerB"))
|
||||||
|
.addEqualityGroup(new RingHashConfig(1, 2, ""))
|
||||||
|
.testEquals();
|
||||||
|
}
|
||||||
|
|
||||||
private List<Subchannel> initializeLbSubchannels(RingHashConfig config,
|
private List<Subchannel> initializeLbSubchannels(RingHashConfig config,
|
||||||
List<EquivalentAddressGroup> servers, InitializationFlags... initFlags) {
|
List<EquivalentAddressGroup> servers, InitializationFlags... initFlags) {
|
||||||
|
|
||||||
|
|
|
@ -17,7 +17,7 @@
|
||||||
|
|
||||||
set -e
|
set -e
|
||||||
# import VERSION from one of the google internal CLs
|
# import VERSION from one of the google internal CLs
|
||||||
VERSION=024c85f92f20cab567a83acc50934c7f9711d124
|
VERSION=2ac532fd44436293585084f8d94c6bdb17835af0
|
||||||
DOWNLOAD_URL="https://github.com/cncf/xds/archive/${VERSION}.tar.gz"
|
DOWNLOAD_URL="https://github.com/cncf/xds/archive/${VERSION}.tar.gz"
|
||||||
DOWNLOAD_BASE_DIR="xds-${VERSION}"
|
DOWNLOAD_BASE_DIR="xds-${VERSION}"
|
||||||
SOURCE_PROTO_BASE_DIR="${DOWNLOAD_BASE_DIR}"
|
SOURCE_PROTO_BASE_DIR="${DOWNLOAD_BASE_DIR}"
|
||||||
|
@ -40,6 +40,7 @@ xds/annotations/v3/versioning.proto
|
||||||
xds/core/v3/authority.proto
|
xds/core/v3/authority.proto
|
||||||
xds/core/v3/collection_entry.proto
|
xds/core/v3/collection_entry.proto
|
||||||
xds/core/v3/context_params.proto
|
xds/core/v3/context_params.proto
|
||||||
|
xds/core/v3/cidr.proto
|
||||||
xds/core/v3/extension.proto
|
xds/core/v3/extension.proto
|
||||||
xds/core/v3/resource_locator.proto
|
xds/core/v3/resource_locator.proto
|
||||||
xds/core/v3/resource_name.proto
|
xds/core/v3/resource_name.proto
|
||||||
|
|
|
@ -10,7 +10,7 @@ option go_package = "github.com/cncf/xds/go/xds/data/orca/v3";
|
||||||
import "validate/validate.proto";
|
import "validate/validate.proto";
|
||||||
|
|
||||||
// See section `ORCA load report format` of the design document in
|
// See section `ORCA load report format` of the design document in
|
||||||
// :ref:`https://github.com/envoyproxy/envoy/issues/6614`.
|
// https://github.com/envoyproxy/envoy/issues/6614.
|
||||||
|
|
||||||
message OrcaLoadReport {
|
message OrcaLoadReport {
|
||||||
// CPU utilization expressed as a fraction of available CPU resources. This
|
// CPU utilization expressed as a fraction of available CPU resources. This
|
||||||
|
|
|
@ -2,9 +2,7 @@ syntax = "proto3";
|
||||||
|
|
||||||
package xds.type.matcher.v3;
|
package xds.type.matcher.v3;
|
||||||
|
|
||||||
import "xds/annotations/v3/status.proto";
|
|
||||||
import "xds/type/v3/cel.proto";
|
import "xds/type/v3/cel.proto";
|
||||||
|
|
||||||
import "validate/validate.proto";
|
import "validate/validate.proto";
|
||||||
|
|
||||||
option java_package = "com.github.xds.type.matcher.v3";
|
option java_package = "com.github.xds.type.matcher.v3";
|
||||||
|
@ -12,8 +10,6 @@ option java_outer_classname = "CelProto";
|
||||||
option java_multiple_files = true;
|
option java_multiple_files = true;
|
||||||
option go_package = "github.com/cncf/xds/go/xds/type/matcher/v3";
|
option go_package = "github.com/cncf/xds/go/xds/type/matcher/v3";
|
||||||
|
|
||||||
option (xds.annotations.v3.file_status).work_in_progress = true;
|
|
||||||
|
|
||||||
// [#protodoc-title: Common Expression Language (CEL) matchers]
|
// [#protodoc-title: Common Expression Language (CEL) matchers]
|
||||||
|
|
||||||
// Performs a match by evaluating a `Common Expression Language
|
// Performs a match by evaluating a `Common Expression Language
|
||||||
|
@ -30,8 +26,7 @@ option (xds.annotations.v3.file_status).work_in_progress = true;
|
||||||
// Refer to :ref:`Unified Matcher API <envoy_v3_api_msg_.xds.type.matcher.v3.Matcher>` documentation
|
// Refer to :ref:`Unified Matcher API <envoy_v3_api_msg_.xds.type.matcher.v3.Matcher>` documentation
|
||||||
// for usage details.
|
// for usage details.
|
||||||
//
|
//
|
||||||
// [#comment:TODO(sergiitk): Link HttpAttributesMatchInput + usage example.]
|
// [#comment: envoy.matching.matchers.cel_matcher]
|
||||||
// [#comment:TODO(sergiitk): When implemented, add the extension tag.]
|
|
||||||
message CelMatcher {
|
message CelMatcher {
|
||||||
// Either parsed or checked representation of the CEL program.
|
// Either parsed or checked representation of the CEL program.
|
||||||
type.v3.CelExpression expr_match = 1 [(validate.rules).message = {required: true}];
|
type.v3.CelExpression expr_match = 1 [(validate.rules).message = {required: true}];
|
||||||
|
|
|
@ -2,15 +2,11 @@ syntax = "proto3";
|
||||||
|
|
||||||
package xds.type.matcher.v3;
|
package xds.type.matcher.v3;
|
||||||
|
|
||||||
import "xds/annotations/v3/status.proto";
|
|
||||||
|
|
||||||
option java_package = "com.github.xds.type.matcher.v3";
|
option java_package = "com.github.xds.type.matcher.v3";
|
||||||
option java_outer_classname = "HttpInputsProto";
|
option java_outer_classname = "HttpInputsProto";
|
||||||
option java_multiple_files = true;
|
option java_multiple_files = true;
|
||||||
option go_package = "github.com/cncf/xds/go/xds/type/matcher/v3";
|
option go_package = "github.com/cncf/xds/go/xds/type/matcher/v3";
|
||||||
|
|
||||||
option (xds.annotations.v3.file_status).work_in_progress = true;
|
|
||||||
|
|
||||||
// [#protodoc-title: Common HTTP Inputs]
|
// [#protodoc-title: Common HTTP Inputs]
|
||||||
|
|
||||||
// Specifies that matching should be performed on the set of :ref:`HTTP attributes
|
// Specifies that matching should be performed on the set of :ref:`HTTP attributes
|
||||||
|
@ -22,6 +18,6 @@ option (xds.annotations.v3.file_status).work_in_progress = true;
|
||||||
// Refer to :ref:`Unified Matcher API <envoy_v3_api_msg_.xds.type.matcher.v3.Matcher>` documentation
|
// Refer to :ref:`Unified Matcher API <envoy_v3_api_msg_.xds.type.matcher.v3.Matcher>` documentation
|
||||||
// for usage details.
|
// for usage details.
|
||||||
//
|
//
|
||||||
// [#comment:TODO(sergiitk): When implemented, add the extension tag.]
|
// [#comment: envoy.matching.inputs.cel_data_input]
|
||||||
message HttpAttributesCelMatchInput {
|
message HttpAttributesCelMatchInput {
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,7 +2,6 @@ syntax = "proto3";
|
||||||
|
|
||||||
package xds.type.matcher.v3;
|
package xds.type.matcher.v3;
|
||||||
|
|
||||||
import "xds/annotations/v3/status.proto";
|
|
||||||
import "xds/core/v3/extension.proto";
|
import "xds/core/v3/extension.proto";
|
||||||
import "xds/type/matcher/v3/string.proto";
|
import "xds/type/matcher/v3/string.proto";
|
||||||
|
|
||||||
|
@ -21,8 +20,6 @@ option go_package = "github.com/cncf/xds/go/xds/type/matcher/v3";
|
||||||
// As an on_no_match might result in another matching tree being evaluated, this process
|
// As an on_no_match might result in another matching tree being evaluated, this process
|
||||||
// might repeat several times until the final OnMatch (or no match) is decided.
|
// might repeat several times until the final OnMatch (or no match) is decided.
|
||||||
message Matcher {
|
message Matcher {
|
||||||
option (xds.annotations.v3.message_status).work_in_progress = true;
|
|
||||||
|
|
||||||
// What to do if a match is successful.
|
// What to do if a match is successful.
|
||||||
message OnMatch {
|
message OnMatch {
|
||||||
oneof on_match {
|
oneof on_match {
|
||||||
|
@ -38,6 +35,14 @@ message Matcher {
|
||||||
// Protocol-specific action to take.
|
// Protocol-specific action to take.
|
||||||
core.v3.TypedExtensionConfig action = 2;
|
core.v3.TypedExtensionConfig action = 2;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// If true and the Matcher matches, the action will be taken but the caller
|
||||||
|
// will behave as if the Matcher did not match. A subsequent matcher or
|
||||||
|
// on_no_match action will be used instead.
|
||||||
|
// This field is not supported in all contexts in which the matcher API is
|
||||||
|
// used. If this field is set in a context in which it's not supported,
|
||||||
|
// the resource will be rejected.
|
||||||
|
bool keep_matching = 3;
|
||||||
}
|
}
|
||||||
|
|
||||||
// A linear list of field matchers.
|
// A linear list of field matchers.
|
||||||
|
|
|
@ -47,6 +47,13 @@ message CelExpression {
|
||||||
//
|
//
|
||||||
// If set, takes precedence over ``cel_expr_parsed``.
|
// If set, takes precedence over ``cel_expr_parsed``.
|
||||||
cel.expr.CheckedExpr cel_expr_checked = 4;
|
cel.expr.CheckedExpr cel_expr_checked = 4;
|
||||||
|
|
||||||
|
// Unparsed expression in string form. For example, ``request.headers['x-env'] == 'prod'`` will
|
||||||
|
// get ``x-env`` header value and compare it with ``prod``.
|
||||||
|
// Check the `Common Expression Language <https://github.com/google/cel-spec>`_ for more details.
|
||||||
|
//
|
||||||
|
// If set, takes precedence over ``cel_expr_parsed`` and ``cel_expr_checked``.
|
||||||
|
string cel_expr_string = 5;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Extracts a string by evaluating a `Common Expression Language
|
// Extracts a string by evaluating a `Common Expression Language
|
||||||
|
|
Loading…
Reference in New Issue