mirror of https://github.com/grpc/grpc-java.git
android: Add UDSChannelBuilder
Allows using Android's LocalSocket via a Socket adapter. Such an adapter isn't generally 100% safe, since some methods may not have any effect, but we know what methods are called by gRPC's okhttp transport and can update the adapter or the transport as appropriate.
This commit is contained in:
parent
cc03b480b8
commit
915c706dec
|
@ -52,6 +52,10 @@ android {
|
|||
disable 'InvalidPackage', 'HardcodedText', 'UsingOnClickInXml',
|
||||
'MissingClass' // https://github.com/grpc/grpc-java/issues/8799
|
||||
}
|
||||
packagingOptions {
|
||||
exclude 'META-INF/INDEX.LIST'
|
||||
exclude 'META-INF/io.netty.versions.properties'
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
|
@ -60,7 +64,8 @@ dependencies {
|
|||
implementation libraries.androidx.annotation
|
||||
implementation 'com.google.android.gms:play-services-base:18.0.1'
|
||||
|
||||
implementation project(':grpc-auth'),
|
||||
implementation project(':grpc-android'),
|
||||
project(':grpc-auth'),
|
||||
project(':grpc-census'),
|
||||
project(':grpc-okhttp'),
|
||||
project(':grpc-protobuf-lite'),
|
||||
|
@ -69,6 +74,7 @@ dependencies {
|
|||
libraries.hdrhistogram,
|
||||
libraries.junit,
|
||||
libraries.truth,
|
||||
libraries.androidx.test.rules,
|
||||
libraries.opencensus.contrib.grpc.metrics
|
||||
|
||||
implementation (libraries.google.auth.oauth2Http) {
|
||||
|
@ -81,7 +87,8 @@ dependencies {
|
|||
|
||||
compileOnly libraries.javax.annotation
|
||||
|
||||
androidTestImplementation 'androidx.test.ext:junit:1.1.3',
|
||||
androidTestImplementation project(':grpc-netty'),
|
||||
'androidx.test.ext:junit:1.1.3',
|
||||
'androidx.test:runner:1.4.0'
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,134 @@
|
|||
/*
|
||||
* Copyright 2021 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.android.integrationtest;
|
||||
|
||||
import static java.util.concurrent.TimeUnit.SECONDS;
|
||||
import static org.junit.Assert.assertEquals;
|
||||
|
||||
import android.net.LocalSocketAddress.Namespace;
|
||||
import androidx.test.InstrumentationRegistry;
|
||||
import androidx.test.rule.ActivityTestRule;
|
||||
import androidx.test.runner.AndroidJUnit4;
|
||||
import com.google.common.util.concurrent.SettableFuture;
|
||||
import io.grpc.Server;
|
||||
import io.grpc.android.UdsChannelBuilder;
|
||||
import io.grpc.android.integrationtest.InteropTask.Listener;
|
||||
import io.grpc.netty.NettyServerBuilder;
|
||||
import io.grpc.testing.integration.TestServiceImpl;
|
||||
import java.io.IOException;
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.concurrent.ScheduledExecutorService;
|
||||
import org.junit.After;
|
||||
import org.junit.Before;
|
||||
import org.junit.Rule;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
|
||||
/**
|
||||
* Tests for channels created with {@link UdsChannelBuilder}. The UDS Channel is only meant to talk
|
||||
* to Unix Domain Socket endpoints on servers that are on-device, so a {@link LocalTestServer} is
|
||||
* set up to expose a UDS endpoint.
|
||||
*/
|
||||
@RunWith(AndroidJUnit4.class)
|
||||
public class UdsChannelInteropTest {
|
||||
private static final int TIMEOUT_SECONDS = 150;
|
||||
|
||||
private static final String UDS_PATH = "udspath";
|
||||
private String testCase;
|
||||
|
||||
private Server server;
|
||||
private UdsTcpEndpointConnector endpointConnector;
|
||||
|
||||
private ScheduledExecutorService executor = Executors.newScheduledThreadPool(2);
|
||||
|
||||
// Ensures Looper is initialized for tests running on API level 15. Otherwise instantiating an
|
||||
// AsyncTask throws an exception.
|
||||
@Rule
|
||||
public ActivityTestRule<TesterActivity> activityRule =
|
||||
new ActivityTestRule<TesterActivity>(TesterActivity.class);
|
||||
|
||||
@Before
|
||||
public void setUp() throws IOException {
|
||||
testCase = InstrumentationRegistry.getArguments().getString("test_case", "all");
|
||||
|
||||
// Start local server.
|
||||
server =
|
||||
NettyServerBuilder.forPort(0)
|
||||
.maxInboundMessageSize(16 * 1024 * 1024)
|
||||
.addService(new TestServiceImpl(executor))
|
||||
.build();
|
||||
server.start();
|
||||
|
||||
// Connect uds endpoint to server's endpoint.
|
||||
endpointConnector = new UdsTcpEndpointConnector(UDS_PATH, "0.0.0.0", server.getPort());
|
||||
endpointConnector.start();
|
||||
}
|
||||
|
||||
@After
|
||||
public void teardown() {
|
||||
server.shutdownNow();
|
||||
endpointConnector.shutDown();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void interopTests() throws Exception {
|
||||
if (testCase.equals("all")) {
|
||||
runTest("empty_unary");
|
||||
runTest("large_unary");
|
||||
runTest("client_streaming");
|
||||
runTest("server_streaming");
|
||||
runTest("ping_pong");
|
||||
runTest("empty_stream");
|
||||
runTest("cancel_after_begin");
|
||||
runTest("cancel_after_first_response");
|
||||
runTest("full_duplex_call_should_succeed");
|
||||
runTest("half_duplex_call_should_succeed");
|
||||
runTest("server_streaming_should_be_flow_controlled");
|
||||
runTest("very_large_request");
|
||||
runTest("very_large_response");
|
||||
runTest("deadline_not_exceeded");
|
||||
runTest("deadline_exceeded");
|
||||
runTest("deadline_exceeded_server_streaming");
|
||||
runTest("unimplemented_method");
|
||||
runTest("timeout_on_sleeping_server");
|
||||
runTest("graceful_shutdown");
|
||||
} else {
|
||||
runTest(testCase);
|
||||
}
|
||||
}
|
||||
|
||||
private void runTest(String testCase) throws Exception {
|
||||
final SettableFuture<String> resultFuture = SettableFuture.create();
|
||||
InteropTask.Listener listener =
|
||||
new Listener() {
|
||||
@Override
|
||||
public void onComplete(String result) {
|
||||
resultFuture.set(result);
|
||||
}
|
||||
};
|
||||
|
||||
new InteropTask(
|
||||
listener,
|
||||
UdsChannelBuilder.forPath(UDS_PATH, Namespace.ABSTRACT)
|
||||
.maxInboundMessageSize(16 * 1024 * 1024)
|
||||
.build(),
|
||||
testCase)
|
||||
.execute();
|
||||
String result = resultFuture.get(TIMEOUT_SECONDS, SECONDS);
|
||||
assertEquals(testCase + " failed", InteropTask.SUCCESS_MESSAGE, result);
|
||||
}
|
||||
}
|
|
@ -2,13 +2,16 @@
|
|||
<manifest xmlns:android="http://schemas.android.com/apk/res/android" >
|
||||
|
||||
<uses-permission android:name="android.permission.INTERNET" />
|
||||
<!-- For UDS -->
|
||||
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
|
||||
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
|
||||
|
||||
<application
|
||||
android:allowBackup="true"
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
android:label="@string/app_name"
|
||||
android:theme="@style/Base.V7.Theme.AppCompat.Light"
|
||||
android:name="androidx.multidex.MultiDexApplication" >
|
||||
android:name="androidx.multidex.MultiDexApplication">
|
||||
<activity
|
||||
android:name=".TesterActivity"
|
||||
android:exported="true"
|
||||
|
|
|
@ -18,6 +18,7 @@ package io.grpc.android.integrationtest;
|
|||
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.net.LocalSocketAddress.Namespace;
|
||||
import android.os.Bundle;
|
||||
import android.text.TextUtils;
|
||||
import android.util.Log;
|
||||
|
@ -30,6 +31,8 @@ import android.widget.TextView;
|
|||
import androidx.appcompat.app.AppCompatActivity;
|
||||
import com.google.android.gms.security.ProviderInstaller;
|
||||
import io.grpc.ManagedChannel;
|
||||
import io.grpc.android.UdsChannelBuilder;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
@ -41,9 +44,13 @@ public class TesterActivity extends AppCompatActivity
|
|||
private List<Button> buttons;
|
||||
private EditText hostEdit;
|
||||
private EditText portEdit;
|
||||
private CheckBox useUdsCheckBox;
|
||||
private EditText udsEdit;
|
||||
private TextView resultText;
|
||||
private CheckBox testCertCheckBox;
|
||||
|
||||
private UdsTcpEndpointConnector endpointConnector;
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
|
@ -57,6 +64,8 @@ public class TesterActivity extends AppCompatActivity
|
|||
|
||||
hostEdit = (EditText) findViewById(R.id.host_edit_text);
|
||||
portEdit = (EditText) findViewById(R.id.port_edit_text);
|
||||
useUdsCheckBox = (CheckBox) findViewById(R.id.use_uds_checkbox);
|
||||
udsEdit = (EditText) findViewById(R.id.uds_proxy_edit_text);
|
||||
resultText = (TextView) findViewById(R.id.grpc_response_text);
|
||||
testCertCheckBox = (CheckBox) findViewById(R.id.test_cert_checkbox);
|
||||
|
||||
|
@ -65,6 +74,16 @@ public class TesterActivity extends AppCompatActivity
|
|||
enableButtons(false);
|
||||
}
|
||||
|
||||
/** Click handler for unix domain socket. */
|
||||
public void enableUds(View view) {
|
||||
boolean enabled = ((CheckBox) view).isChecked();
|
||||
udsEdit.setEnabled(enabled);
|
||||
testCertCheckBox.setEnabled(!enabled);
|
||||
if (enabled) {
|
||||
testCertCheckBox.setChecked(false);
|
||||
}
|
||||
}
|
||||
|
||||
public void startEmptyUnary(View view) {
|
||||
startTest("empty_unary");
|
||||
}
|
||||
|
@ -93,6 +112,10 @@ public class TesterActivity extends AppCompatActivity
|
|||
|
||||
@Override
|
||||
public void onComplete(String result) {
|
||||
if (endpointConnector != null) {
|
||||
endpointConnector.shutDown();
|
||||
endpointConnector = null;
|
||||
}
|
||||
resultText.setText(result);
|
||||
enableButtons(true);
|
||||
}
|
||||
|
@ -106,6 +129,9 @@ public class TesterActivity extends AppCompatActivity
|
|||
String host = hostEdit.getText().toString();
|
||||
String portStr = portEdit.getText().toString();
|
||||
int port = TextUtils.isEmpty(portStr) ? 8080 : Integer.valueOf(portStr);
|
||||
boolean udsEnabled = useUdsCheckBox.isChecked();
|
||||
String udsPath =
|
||||
TextUtils.isEmpty(udsEdit.getText()) ? "default" : udsEdit.getText().toString();
|
||||
|
||||
String serverHostOverride;
|
||||
InputStream testCert;
|
||||
|
@ -116,10 +142,27 @@ public class TesterActivity extends AppCompatActivity
|
|||
serverHostOverride = null;
|
||||
testCert = null;
|
||||
}
|
||||
ManagedChannel channel =
|
||||
TesterOkHttpChannelBuilder.build(host, port, serverHostOverride, true, testCert);
|
||||
|
||||
new InteropTask(this, channel, testCase).execute();
|
||||
// Create Channel
|
||||
ManagedChannel channel;
|
||||
if (udsEnabled) {
|
||||
channel = UdsChannelBuilder.forPath(udsPath, Namespace.ABSTRACT).build();
|
||||
} else {
|
||||
channel = TesterOkHttpChannelBuilder.build(host, port, serverHostOverride, true, testCert);
|
||||
}
|
||||
|
||||
// Port-forward uds local port to server exposing tcp endpoint.
|
||||
if (udsEnabled) {
|
||||
endpointConnector = new UdsTcpEndpointConnector(udsPath, host, port);
|
||||
try {
|
||||
endpointConnector.start();
|
||||
} catch (IOException e) {
|
||||
Log.e(LOG_TAG, "Failed to start UDS-TCP Endpoint Connector.");
|
||||
}
|
||||
}
|
||||
|
||||
// Start Test.
|
||||
new InteropTask(TesterActivity.this, channel, testCase).execute();
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
|
@ -0,0 +1,198 @@
|
|||
/*
|
||||
* Copyright 2021 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.android.integrationtest;
|
||||
|
||||
import static java.util.concurrent.TimeUnit.SECONDS;
|
||||
|
||||
import android.net.LocalServerSocket;
|
||||
import android.net.LocalSocket;
|
||||
import android.util.Log;
|
||||
import java.io.DataInputStream;
|
||||
import java.io.DataOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.net.InetSocketAddress;
|
||||
import java.net.Socket;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.concurrent.LinkedBlockingQueue;
|
||||
import java.util.concurrent.ThreadPoolExecutor;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
|
||||
/**
|
||||
* Funnels traffic between a given UDS endpoint and a local TCP endpoint. A client binds to the UDS
|
||||
* endpoint, but effectively communicates with the TCP endpoint.
|
||||
*/
|
||||
public class UdsTcpEndpointConnector {
|
||||
|
||||
private static final String LOG_TAG = "EndpointConnector";
|
||||
|
||||
// Discard-policy, to allow dropping tasks that were received immediately after shutDown()
|
||||
private final ThreadPoolExecutor executor =
|
||||
new ThreadPoolExecutor(
|
||||
5,
|
||||
10,
|
||||
1,
|
||||
SECONDS,
|
||||
new LinkedBlockingQueue<>(),
|
||||
new ThreadPoolExecutor.DiscardOldestPolicy());
|
||||
|
||||
private final String udsPath;
|
||||
|
||||
private final String host;
|
||||
private final int port;
|
||||
private InetSocketAddress socketAddress;
|
||||
|
||||
private LocalServerSocket clientAcceptor;
|
||||
|
||||
private volatile boolean shutDownRequested = false;
|
||||
private volatile boolean shutDownComplete = false;
|
||||
|
||||
/** Listen on udsPath and forward connections to host:port. */
|
||||
public UdsTcpEndpointConnector(String udsPath, String host, int port) {
|
||||
this.udsPath = udsPath;
|
||||
this.host = host;
|
||||
this.port = port;
|
||||
}
|
||||
|
||||
/** Start listening and accept connections. */
|
||||
public void start() throws IOException {
|
||||
clientAcceptor = new LocalServerSocket(udsPath);
|
||||
executor.execute(
|
||||
new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
Log.i(LOG_TAG, "Starting connection from " + udsPath + " to " + socketAddress);
|
||||
socketAddress = new InetSocketAddress(host, port);
|
||||
while (!shutDownRequested) {
|
||||
try {
|
||||
LocalSocket clientSocket = clientAcceptor.accept();
|
||||
if (shutDownRequested) {
|
||||
// Check if shut down during blocking accept().
|
||||
clientSocket.close();
|
||||
shutDownComplete = true;
|
||||
break;
|
||||
}
|
||||
Socket serverSocket = new Socket();
|
||||
serverSocket.connect(socketAddress);
|
||||
startWorkers(clientSocket, serverSocket);
|
||||
} catch (IOException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/** Stop listening and release resources. */
|
||||
public void shutDown() {
|
||||
Log.i(LOG_TAG, "Shutting down connection from " + udsPath + " to " + socketAddress);
|
||||
shutDownRequested = true;
|
||||
|
||||
try {
|
||||
// Upon clientAcceptor.close(), clientAcceptor.accept() continues to block.
|
||||
// Thus, once shutDownRequested=true, must send a connection request to unblock accept().
|
||||
LocalSocket localSocket = new LocalSocket();
|
||||
localSocket.connect(clientAcceptor.getLocalSocketAddress());
|
||||
localSocket.close();
|
||||
clientAcceptor.close();
|
||||
} catch (IOException e) {
|
||||
Log.w(LOG_TAG, "Failed to close LocalServerSocket", e);
|
||||
}
|
||||
executor.shutdownNow();
|
||||
}
|
||||
|
||||
public boolean isShutdown() {
|
||||
return shutDownComplete && executor.isShutdown();
|
||||
}
|
||||
|
||||
private void startWorkers(LocalSocket clientSocket, Socket serverSocket) throws IOException {
|
||||
DataInputStream clientIn = new DataInputStream(clientSocket.getInputStream());
|
||||
DataOutputStream clientOut = new DataOutputStream(serverSocket.getOutputStream());
|
||||
DataInputStream serverIn = new DataInputStream(serverSocket.getInputStream());
|
||||
DataOutputStream serverOut = new DataOutputStream(clientSocket.getOutputStream());
|
||||
|
||||
AtomicInteger completionCount = new AtomicInteger(0);
|
||||
StreamConnector.Listener cleanupListener =
|
||||
new StreamConnector.Listener() {
|
||||
@Override
|
||||
public void onFinished() {
|
||||
if (completionCount.incrementAndGet() == 2) {
|
||||
try {
|
||||
serverSocket.close();
|
||||
clientSocket.close();
|
||||
} catch (IOException e) {
|
||||
Log.e(LOG_TAG, "Failed to clean up connected sockets.", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
executor.execute(new StreamConnector(clientIn, clientOut).addListener(cleanupListener));
|
||||
executor.execute(new StreamConnector(serverIn, serverOut).addListener(cleanupListener));
|
||||
}
|
||||
|
||||
/**
|
||||
* Funnels everything that comes in to a DataInputStream into an DataOutputStream, until the
|
||||
* DataInputStream is closed. (detected by IOException).
|
||||
*/
|
||||
private static final class StreamConnector implements Runnable {
|
||||
|
||||
interface Listener {
|
||||
void onFinished();
|
||||
}
|
||||
|
||||
private static final int BUFFER_SIZE = 1000;
|
||||
|
||||
private final DataInputStream in;
|
||||
private final DataOutputStream out;
|
||||
private final byte[] buffer = new byte[BUFFER_SIZE];
|
||||
|
||||
private boolean finished = false;
|
||||
|
||||
private final Collection<Listener> listeners = new ArrayList<>();
|
||||
|
||||
StreamConnector(DataInputStream in, DataOutputStream out) {
|
||||
this.in = in;
|
||||
this.out = out;
|
||||
}
|
||||
|
||||
StreamConnector addListener(Listener listener) {
|
||||
listeners.add(listener);
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
while (!finished) {
|
||||
int bytesRead;
|
||||
try {
|
||||
bytesRead = in.read(buffer);
|
||||
if (bytesRead == -1) {
|
||||
finished = true;
|
||||
out.close();
|
||||
continue;
|
||||
}
|
||||
out.write(buffer, 0, bytesRead);
|
||||
} catch (IOException e) {
|
||||
finished = true;
|
||||
}
|
||||
}
|
||||
for (StreamConnector.Listener listener : listeners) {
|
||||
listener.onFinished();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,5 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<selector xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<item android:color="#AAA" android:state_focused="false"/>
|
||||
<item android:color="#000" android:state_focused="true"/>
|
||||
</selector>
|
|
@ -28,6 +28,25 @@
|
|||
/>
|
||||
</LinearLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal">
|
||||
<CheckBox android:id="@+id/use_uds_checkbox"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:onClick="enableUds"
|
||||
android:textColor="@color/focus"
|
||||
android:text="Use UDS (SSL not supported)"/>
|
||||
<EditText
|
||||
android:id="@+id/uds_proxy_edit_text"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_width="match_parent"
|
||||
android:enabled="false"
|
||||
android:hint="Enter Unix Domain Socket Abstract Namespace Address"
|
||||
/>
|
||||
</LinearLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
|
@ -36,6 +55,7 @@
|
|||
<CheckBox android:id="@+id/test_cert_checkbox"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:textColor="@color/focus"
|
||||
android:text="Use Test Cert"
|
||||
/>
|
||||
</LinearLayout>
|
||||
|
|
|
@ -0,0 +1,92 @@
|
|||
/*
|
||||
* Copyright 2021 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.android;
|
||||
|
||||
import android.net.LocalSocketAddress.Namespace;
|
||||
import io.grpc.ChannelCredentials;
|
||||
import io.grpc.ExperimentalApi;
|
||||
import io.grpc.InsecureChannelCredentials;
|
||||
import io.grpc.ManagedChannelBuilder;
|
||||
import java.lang.reflect.InvocationTargetException;
|
||||
import javax.annotation.Nullable;
|
||||
import javax.net.SocketFactory;
|
||||
|
||||
/**
|
||||
* Creates a UDS channel by passing in a specialized SocketFactory into an OkHttpChannelBuilder. The
|
||||
* UdsSockets produced by this factory communicate over Android's LocalSockets.
|
||||
*
|
||||
* <p>Example Usage <code>
|
||||
* Channel channel = UdsChannelBuilder.forPath("/data/data/my.app/app.socket",
|
||||
* Namespace.FILESYSTEM).build();
|
||||
* stub = MyService.newStub(channel);
|
||||
* </code>
|
||||
*
|
||||
* <p>This class uses a safe-for-production hack to workaround NameResolver's inability to safely
|
||||
* return non-IP SocketAddress types. The hack simply ignores the name resolver results and connects
|
||||
* to the UDS name provided during construction instead. This class is expected to be replaced with
|
||||
* a `unix:` name resolver when possible.
|
||||
*/
|
||||
@ExperimentalApi("A stopgap. Not intended to be stabilized")
|
||||
public final class UdsChannelBuilder {
|
||||
@Nullable
|
||||
@SuppressWarnings("rawtypes")
|
||||
private static final Class<? extends ManagedChannelBuilder> OKHTTP_CHANNEL_BUILDER_CLASS =
|
||||
findOkHttp();
|
||||
|
||||
@SuppressWarnings("rawtypes")
|
||||
private static Class<? extends ManagedChannelBuilder> findOkHttp() {
|
||||
try {
|
||||
return Class.forName("io.grpc.okhttp.OkHttpChannelBuilder")
|
||||
.asSubclass(ManagedChannelBuilder.class);
|
||||
} catch (ClassNotFoundException e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a channel to the UDS endpoint specified by the file-path.
|
||||
*
|
||||
* @param path unix file system path to use for Unix Domain Socket.
|
||||
* @param namespace the type of the namespace that the path belongs to.
|
||||
*/
|
||||
public static ManagedChannelBuilder<?> forPath(String path, Namespace namespace) {
|
||||
if (OKHTTP_CHANNEL_BUILDER_CLASS == null) {
|
||||
throw new UnsupportedOperationException("OkHttpChannelBuilder not found on the classpath");
|
||||
}
|
||||
try {
|
||||
// Target 'dns:///localhost' is unused, but necessary as an argument for OkHttpChannelBuilder.
|
||||
// TLS is unsupported because Conscrypt assumes the platform Socket implementation to improve
|
||||
// performance by using the file descriptor directly.
|
||||
Object o = OKHTTP_CHANNEL_BUILDER_CLASS
|
||||
.getMethod("forTarget", String.class, ChannelCredentials.class)
|
||||
.invoke(null, "dns:///localhost", InsecureChannelCredentials.create());
|
||||
ManagedChannelBuilder<?> builder = OKHTTP_CHANNEL_BUILDER_CLASS.cast(o);
|
||||
OKHTTP_CHANNEL_BUILDER_CLASS
|
||||
.getMethod("socketFactory", SocketFactory.class)
|
||||
.invoke(builder, new UdsSocketFactory(path, namespace));
|
||||
return builder;
|
||||
} catch (IllegalAccessException e) {
|
||||
throw new RuntimeException("Failed to create OkHttpChannelBuilder", e);
|
||||
} catch (NoSuchMethodException e) {
|
||||
throw new RuntimeException("Failed to create OkHttpChannelBuilder", e);
|
||||
} catch (InvocationTargetException e) {
|
||||
throw new RuntimeException("Failed to create OkHttpChannelBuilder", e);
|
||||
}
|
||||
}
|
||||
|
||||
private UdsChannelBuilder() {}
|
||||
}
|
|
@ -0,0 +1,312 @@
|
|||
/*
|
||||
* Copyright 2021 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.android;
|
||||
|
||||
import android.net.LocalSocket;
|
||||
import android.net.LocalSocketAddress;
|
||||
import com.google.errorprone.annotations.concurrent.GuardedBy;
|
||||
import java.io.FilterInputStream;
|
||||
import java.io.FilterOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
import java.net.InetAddress;
|
||||
import java.net.Socket;
|
||||
import java.net.SocketAddress;
|
||||
import java.net.SocketException;
|
||||
import java.nio.channels.SocketChannel;
|
||||
|
||||
/**
|
||||
* Adapter from Android's LocalSocket to Socket. This class is only needed by grpc-okhttp, so the
|
||||
* adapter only has to support the things that grcp-okhttp uses. It is fine to support trivial
|
||||
* things unused by the transport, to be less likely to break as the transport usage changes, but it
|
||||
* is also unnecessary. It's okay to stretch the truth or lie when necessary. For example, little
|
||||
* hurts with {@link #setTcpNoDelay(boolean)} being a noop since unix domain sockets don't have such
|
||||
* unnecessary delays.
|
||||
*/
|
||||
@SuppressWarnings("UnsynchronizedOverridesSynchronized") // Rely on LocalSocket's synchronization
|
||||
class UdsSocket extends Socket {
|
||||
|
||||
private final LocalSocket localSocket;
|
||||
private final LocalSocketAddress localSocketAddress;
|
||||
|
||||
@GuardedBy("this")
|
||||
private boolean closed = false;
|
||||
|
||||
@GuardedBy("this")
|
||||
private boolean inputShutdown = false;
|
||||
|
||||
@GuardedBy("this")
|
||||
private boolean outputShutdown = false;
|
||||
|
||||
public UdsSocket(LocalSocketAddress localSocketAddress) {
|
||||
this.localSocketAddress = localSocketAddress;
|
||||
localSocket = new LocalSocket();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void bind(SocketAddress bindpoint) {
|
||||
// no-op
|
||||
}
|
||||
|
||||
@Override
|
||||
public synchronized void close() throws IOException {
|
||||
if (closed) {
|
||||
return;
|
||||
}
|
||||
if (!inputShutdown) {
|
||||
shutdownInput();
|
||||
}
|
||||
if (!outputShutdown) {
|
||||
shutdownOutput();
|
||||
}
|
||||
localSocket.close();
|
||||
closed = true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void connect(SocketAddress endpoint) throws IOException {
|
||||
localSocket.connect(localSocketAddress);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void connect(SocketAddress endpoint, int timeout) throws IOException {
|
||||
localSocket.connect(localSocketAddress, timeout);
|
||||
}
|
||||
|
||||
@Override
|
||||
public SocketChannel getChannel() {
|
||||
throw new UnsupportedOperationException("getChannel() not supported");
|
||||
}
|
||||
|
||||
@Override
|
||||
public InetAddress getInetAddress() {
|
||||
throw new UnsupportedOperationException("getInetAddress() not supported");
|
||||
}
|
||||
|
||||
@Override
|
||||
public InputStream getInputStream() throws IOException {
|
||||
return new FilterInputStream(localSocket.getInputStream()) {
|
||||
@Override
|
||||
public void close() throws IOException {
|
||||
UdsSocket.this.close();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean getKeepAlive() {
|
||||
throw new UnsupportedOperationException("Unsupported operation getKeepAlive()");
|
||||
}
|
||||
|
||||
@Override
|
||||
public InetAddress getLocalAddress() {
|
||||
throw new UnsupportedOperationException("Unsupported operation getLocalAddress()");
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getLocalPort() {
|
||||
throw new UnsupportedOperationException("Unsupported operation getLocalPort()");
|
||||
}
|
||||
|
||||
@Override
|
||||
public SocketAddress getLocalSocketAddress() {
|
||||
return new SocketAddress() {};
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean getOOBInline() {
|
||||
throw new UnsupportedOperationException("Unsupported operation getOOBInline()");
|
||||
}
|
||||
|
||||
@Override
|
||||
public OutputStream getOutputStream() throws IOException {
|
||||
return new FilterOutputStream(localSocket.getOutputStream()) {
|
||||
@Override
|
||||
public void close() throws IOException {
|
||||
UdsSocket.this.close();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getPort() {
|
||||
throw new UnsupportedOperationException("Unsupported operation getPort()");
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getReceiveBufferSize() throws SocketException {
|
||||
try {
|
||||
return localSocket.getReceiveBufferSize();
|
||||
} catch (IOException e) {
|
||||
throw toSocketException(e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public SocketAddress getRemoteSocketAddress() {
|
||||
return new SocketAddress() {};
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean getReuseAddress() {
|
||||
throw new UnsupportedOperationException("Unsupported operation getReuseAddress()");
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getSendBufferSize() throws SocketException {
|
||||
try {
|
||||
return localSocket.getSendBufferSize();
|
||||
} catch (IOException e) {
|
||||
throw toSocketException(e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getSoLinger() {
|
||||
return -1; // unsupported
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getSoTimeout() throws SocketException {
|
||||
try {
|
||||
return localSocket.getSoTimeout();
|
||||
} catch (IOException e) {
|
||||
throw toSocketException(e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean getTcpNoDelay() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getTrafficClass() {
|
||||
throw new UnsupportedOperationException("Unsupported operation getTrafficClass()");
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isBound() {
|
||||
return localSocket.isBound();
|
||||
}
|
||||
|
||||
@Override
|
||||
public synchronized boolean isClosed() {
|
||||
return closed;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isConnected() {
|
||||
return localSocket.isConnected();
|
||||
}
|
||||
|
||||
@Override
|
||||
public synchronized boolean isInputShutdown() {
|
||||
return inputShutdown;
|
||||
}
|
||||
|
||||
@Override
|
||||
public synchronized boolean isOutputShutdown() {
|
||||
return outputShutdown;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void sendUrgentData(int data) {
|
||||
throw new UnsupportedOperationException("Unsupported operation sendUrgentData()");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setKeepAlive(boolean on) {
|
||||
throw new UnsupportedOperationException("Unsupported operation setKeepAlive()");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setOOBInline(boolean on) {
|
||||
throw new UnsupportedOperationException("Unsupported operation setOOBInline()");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setPerformancePreferences(int connectionTime, int latency, int bandwidth) {
|
||||
throw new UnsupportedOperationException("Unsupported operation setPerformancePreferences()");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setReceiveBufferSize(int size) throws SocketException {
|
||||
try {
|
||||
localSocket.setReceiveBufferSize(size);
|
||||
} catch (IOException e) {
|
||||
throw toSocketException(e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setReuseAddress(boolean on) {
|
||||
throw new UnsupportedOperationException("Unsupported operation setReuseAddress()");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setSendBufferSize(int size) throws SocketException {
|
||||
try {
|
||||
localSocket.setSendBufferSize(size);
|
||||
} catch (IOException e) {
|
||||
throw toSocketException(e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setSoLinger(boolean on, int linger) {
|
||||
throw new UnsupportedOperationException("Unsupported operation setSoLinger()");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setSoTimeout(int timeout) throws SocketException {
|
||||
try {
|
||||
localSocket.setSoTimeout(timeout);
|
||||
} catch (IOException e) {
|
||||
throw toSocketException(e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setTcpNoDelay(boolean on) {
|
||||
// no-op
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setTrafficClass(int tc) {
|
||||
throw new UnsupportedOperationException("Unsupported operation setTrafficClass()");
|
||||
}
|
||||
|
||||
@Override
|
||||
public synchronized void shutdownInput() throws IOException {
|
||||
localSocket.shutdownInput();
|
||||
inputShutdown = true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public synchronized void shutdownOutput() throws IOException {
|
||||
localSocket.shutdownOutput();
|
||||
outputShutdown = true;
|
||||
}
|
||||
|
||||
private static SocketException toSocketException(Throwable e) {
|
||||
SocketException se = new SocketException();
|
||||
se.initCause(e);
|
||||
return se;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,77 @@
|
|||
/*
|
||||
* Copyright 2021 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.android;
|
||||
|
||||
import android.net.LocalSocketAddress;
|
||||
import android.net.LocalSocketAddress.Namespace;
|
||||
import java.io.IOException;
|
||||
import java.net.InetAddress;
|
||||
import java.net.InetSocketAddress;
|
||||
import java.net.Socket;
|
||||
import java.net.SocketAddress;
|
||||
import javax.net.SocketFactory;
|
||||
|
||||
/**
|
||||
* A SocketFactory that produces {@link UdsSocket} instances. This is used to provide support for
|
||||
* gRPC channels with an underlying Unix Domain Socket transport.
|
||||
*/
|
||||
class UdsSocketFactory extends SocketFactory {
|
||||
|
||||
private final LocalSocketAddress localSocketAddress;
|
||||
|
||||
public UdsSocketFactory(String path, Namespace namespace) {
|
||||
localSocketAddress = new LocalSocketAddress(path, namespace);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Socket createSocket() throws IOException {
|
||||
return create();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Socket createSocket(String host, int port) throws IOException {
|
||||
return createAndConnect();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Socket createSocket(String host, int port, InetAddress localHost, int localPort)
|
||||
throws IOException {
|
||||
return createAndConnect();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Socket createSocket(InetAddress host, int port) throws IOException {
|
||||
return createAndConnect();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Socket createSocket(InetAddress address, int port, InetAddress localAddress, int localPort)
|
||||
throws IOException {
|
||||
return createAndConnect();
|
||||
}
|
||||
|
||||
private Socket create() {
|
||||
return new UdsSocket(localSocketAddress);
|
||||
}
|
||||
|
||||
private Socket createAndConnect() throws IOException {
|
||||
Socket socket = create();
|
||||
SocketAddress unusedAddress = new InetSocketAddress(0);
|
||||
socket.connect(unusedAddress);
|
||||
return socket;
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue