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:
Ken Katagiri 2021-08-17 12:45:21 -07:00 committed by Eric Anderson
parent cc03b480b8
commit 915c706dec
10 changed files with 897 additions and 6 deletions

View File

@ -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'
}

View File

@ -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);
}
}

View File

@ -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"

View File

@ -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

View File

@ -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();
}
}
}
}

View File

@ -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>

View File

@ -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>

View File

@ -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() {}
}

View File

@ -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;
}
}

View File

@ -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;
}
}