From 915c706dec0fa2e847278f94f33166190ec5537a Mon Sep 17 00:00:00 2001 From: Ken Katagiri Date: Tue, 17 Aug 2021 12:45:21 -0700 Subject: [PATCH] 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. --- android-interop-testing/build.gradle | 11 +- .../UdsChannelInteropTest.java | 134 ++++++++ .../src/main/AndroidManifest.xml | 5 +- .../integrationtest/TesterActivity.java | 49 ++- .../UdsTcpEndpointConnector.java | 198 +++++++++++ .../src/main/res/color/focus.xml | 5 + .../src/main/res/layout/activity_tester.xml | 20 ++ .../io/grpc/android/UdsChannelBuilder.java | 92 ++++++ .../main/java/io/grpc/android/UdsSocket.java | 312 ++++++++++++++++++ .../io/grpc/android/UdsSocketFactory.java | 77 +++++ 10 files changed, 897 insertions(+), 6 deletions(-) create mode 100644 android-interop-testing/src/androidTest/java/io/grpc/android/integrationtest/UdsChannelInteropTest.java create mode 100644 android-interop-testing/src/main/java/io/grpc/android/integrationtest/UdsTcpEndpointConnector.java create mode 100644 android-interop-testing/src/main/res/color/focus.xml create mode 100644 android/src/main/java/io/grpc/android/UdsChannelBuilder.java create mode 100644 android/src/main/java/io/grpc/android/UdsSocket.java create mode 100644 android/src/main/java/io/grpc/android/UdsSocketFactory.java diff --git a/android-interop-testing/build.gradle b/android-interop-testing/build.gradle index 66acc38707..7c353354d7 100644 --- a/android-interop-testing/build.gradle +++ b/android-interop-testing/build.gradle @@ -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' } diff --git a/android-interop-testing/src/androidTest/java/io/grpc/android/integrationtest/UdsChannelInteropTest.java b/android-interop-testing/src/androidTest/java/io/grpc/android/integrationtest/UdsChannelInteropTest.java new file mode 100644 index 0000000000..40b954c00e --- /dev/null +++ b/android-interop-testing/src/androidTest/java/io/grpc/android/integrationtest/UdsChannelInteropTest.java @@ -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 activityRule = + new ActivityTestRule(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 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); + } +} diff --git a/android-interop-testing/src/main/AndroidManifest.xml b/android-interop-testing/src/main/AndroidManifest.xml index 9ccbb23e34..250deb087e 100644 --- a/android-interop-testing/src/main/AndroidManifest.xml +++ b/android-interop-testing/src/main/AndroidManifest.xml @@ -2,13 +2,16 @@ + + + + android:name="androidx.multidex.MultiDexApplication"> 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 diff --git a/android-interop-testing/src/main/java/io/grpc/android/integrationtest/UdsTcpEndpointConnector.java b/android-interop-testing/src/main/java/io/grpc/android/integrationtest/UdsTcpEndpointConnector.java new file mode 100644 index 0000000000..61c1f4f8da --- /dev/null +++ b/android-interop-testing/src/main/java/io/grpc/android/integrationtest/UdsTcpEndpointConnector.java @@ -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 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(); + } + } + } +} diff --git a/android-interop-testing/src/main/res/color/focus.xml b/android-interop-testing/src/main/res/color/focus.xml new file mode 100644 index 0000000000..ced80c1981 --- /dev/null +++ b/android-interop-testing/src/main/res/color/focus.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/android-interop-testing/src/main/res/layout/activity_tester.xml b/android-interop-testing/src/main/res/layout/activity_tester.xml index 9c511cab8d..c5af93cb87 100644 --- a/android-interop-testing/src/main/res/layout/activity_tester.xml +++ b/android-interop-testing/src/main/res/layout/activity_tester.xml @@ -28,6 +28,25 @@ /> + + + + + diff --git a/android/src/main/java/io/grpc/android/UdsChannelBuilder.java b/android/src/main/java/io/grpc/android/UdsChannelBuilder.java new file mode 100644 index 0000000000..e2dc723237 --- /dev/null +++ b/android/src/main/java/io/grpc/android/UdsChannelBuilder.java @@ -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. + * + *

Example Usage + * Channel channel = UdsChannelBuilder.forPath("/data/data/my.app/app.socket", + * Namespace.FILESYSTEM).build(); + * stub = MyService.newStub(channel); + * + * + *

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 OKHTTP_CHANNEL_BUILDER_CLASS = + findOkHttp(); + + @SuppressWarnings("rawtypes") + private static Class 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() {} +} diff --git a/android/src/main/java/io/grpc/android/UdsSocket.java b/android/src/main/java/io/grpc/android/UdsSocket.java new file mode 100644 index 0000000000..bbe19e1ddb --- /dev/null +++ b/android/src/main/java/io/grpc/android/UdsSocket.java @@ -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; + } +} diff --git a/android/src/main/java/io/grpc/android/UdsSocketFactory.java b/android/src/main/java/io/grpc/android/UdsSocketFactory.java new file mode 100644 index 0000000000..93150196d9 --- /dev/null +++ b/android/src/main/java/io/grpc/android/UdsSocketFactory.java @@ -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; + } +}