From b41ebccca76d01e7e4e9c418a5b3a9e99395c1ae Mon Sep 17 00:00:00 2001 From: Jonathan Hess Date: Tue, 23 Apr 2024 16:39:28 -0600 Subject: [PATCH] test: Add a unix socket connector test. Part of #1940 --- core/pom.xml | 6 + .../google/cloud/sql/core/ConnectorTest.java | 39 ++++++ .../cloud/sql/core/FakeUnixSocketServer.java | 128 ++++++++++++++++++ pom.xml | 5 +- 4 files changed, 176 insertions(+), 2 deletions(-) create mode 100644 core/src/test/java/com/google/cloud/sql/core/FakeUnixSocketServer.java diff --git a/core/pom.xml b/core/pom.xml index f2e33d919..f69406226 100644 --- a/core/pom.xml +++ b/core/pom.xml @@ -58,6 +58,12 @@ test + + com.github.jnr + jnr-enxio + 0.32.17 + + com.github.jnr jnr-unixsocket diff --git a/core/src/test/java/com/google/cloud/sql/core/ConnectorTest.java b/core/src/test/java/com/google/cloud/sql/core/ConnectorTest.java index 4d9e2fbe9..aea3973da 100644 --- a/core/src/test/java/com/google/cloud/sql/core/ConnectorTest.java +++ b/core/src/test/java/com/google/cloud/sql/core/ConnectorTest.java @@ -30,6 +30,8 @@ import java.io.IOException; import java.io.InputStreamReader; import java.net.Socket; +import java.nio.file.Files; +import java.nio.file.Path; import java.time.Duration; import java.time.Instant; import java.util.Collections; @@ -118,6 +120,43 @@ public void create_successfulPublicConnection() throws IOException, InterruptedE assertThat(readLine(socket)).isEqualTo(SERVER_MESSAGE); } + private boolean isWindows() { + String os = System.getProperty("os.name").toLowerCase(); + return os.contains("win"); + } + + @Test + public void create_successfulUnixSocketConnection() throws IOException, InterruptedException { + if (isWindows()) { + System.out.println("Skipping unix socket test on Windows."); + return; + } + + Path socketTestDir = Files.createTempDirectory("sockettest"); + Path socketPath = socketTestDir.resolve("test.sock"); + FakeUnixSocketServer unixSocketServer = new FakeUnixSocketServer(socketPath.toString()); + + try { + + ConnectionConfig config = + new ConnectionConfig.Builder() + .withCloudSqlInstance("myProject:myRegion:myInstance") + .withIpTypes("PRIMARY") + .withUnixSocketPath(socketPath.toString()) + .build(); + + unixSocketServer.start(); + + Connector connector = newConnector(config.getConnectorConfig(), 10000); + + Socket socket = connector.connect(config, TEST_MAX_REFRESH_MS); + + assertThat(readLine(socket)).isEqualTo(SERVER_MESSAGE); + } finally { + unixSocketServer.close(); + } + } + @Test public void create_successfulDomainScopedConnection() throws IOException, InterruptedException { FakeSslServer sslServer = new FakeSslServer(); diff --git a/core/src/test/java/com/google/cloud/sql/core/FakeUnixSocketServer.java b/core/src/test/java/com/google/cloud/sql/core/FakeUnixSocketServer.java new file mode 100644 index 000000000..8f1baa327 --- /dev/null +++ b/core/src/test/java/com/google/cloud/sql/core/FakeUnixSocketServer.java @@ -0,0 +1,128 @@ +/* + * Copyright 2024 Google LLC + * + * 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 com.google.cloud.sql.core; + +import java.io.IOException; +import java.nio.ByteBuffer; +import java.nio.channels.SelectionKey; +import java.nio.channels.Selector; +import java.util.Iterator; +import java.util.Set; +import java.util.concurrent.atomic.AtomicBoolean; +import jnr.enxio.channels.NativeSelectorProvider; +import jnr.unixsocket.UnixServerSocketChannel; +import jnr.unixsocket.UnixSocketAddress; +import jnr.unixsocket.UnixSocketChannel; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * This is a simple echo unix socket server adapted from the JNR socket project. + * https://github.com/jnr/jnr-unixsocket/blob/master/src/test/java/jnr/unixsocket/example/UnixServer.java + */ +class FakeUnixSocketServer { + private static Logger log = LoggerFactory.getLogger(FakeUnixSocketServer.class); + private final String path; + private final UnixSocketAddress address; + private UnixServerSocketChannel channel; + private final AtomicBoolean closed = new AtomicBoolean(); + + FakeUnixSocketServer(String path) { + this.path = path; + this.address = new UnixSocketAddress(path); + } + + public void close() throws IOException { + this.closed.set(true); + channel.close(); + } + + public void start() throws IOException { + java.io.File path = new java.io.File(this.path); + path.deleteOnExit(); + + this.channel = UnixServerSocketChannel.open(); + channel.configureBlocking(false); + channel.socket().bind(address); + + Thread t = new Thread(this::run); + t.start(); + } + + public void run() { + log.info("Starting fake unix socket server at path " + this.path); + try { + Selector sel = NativeSelectorProvider.getInstance().openSelector(); + channel.register(sel, SelectionKey.OP_ACCEPT, new ServerActor(channel)); + + log.info("Waiting for connections path " + this.path); + while (!this.closed.get()) { + if (sel.select() > 0) { + Set keys = sel.selectedKeys(); + Iterator iterator = keys.iterator(); + boolean running = false; + boolean cancelled = false; + while (iterator.hasNext()) { + SelectionKey k = iterator.next(); + Actor a = (Actor) k.attachment(); + if (a.rxready()) { + running = true; + } else { + k.cancel(); + cancelled = true; + } + iterator.remove(); + } + if (!running && cancelled) { + log.info("No Actors Running any more"); + break; + } + } + } + } catch (IOException ex) { + log.info("IOException: waiting for sockets", ex); + } + log.info("UnixServer EXIT"); + } + + static interface Actor { + public boolean rxready(); + } + + static final class ServerActor implements Actor { + private final UnixServerSocketChannel channel; + + public ServerActor(UnixServerSocketChannel channel) { + this.channel = channel; + } + + @Override + public final boolean rxready() { + log.info("Handling unix socket connect."); + try { + UnixSocketChannel client = channel.accept(); + client.configureBlocking(false); + ByteBuffer response = ByteBuffer.wrap("HELLO\n".getBytes("UTF-8")); + client.write(response); + log.info("Handling unix socket done."); + return true; + } catch (IOException ex) { + return false; + } + } + } +} diff --git a/pom.xml b/pom.xml index 3264e32ce..773aaf4c6 100644 --- a/pom.xml +++ b/pom.xml @@ -569,7 +569,8 @@ com.google.auth:google-auth-library-credentials Necessary for Unix socket support: - org.ow2.asm:asm-util AND com.github.jnr:jnr-unixsocket + org.ow2.asm:asm-util AND com.github.jnr:jnr-unixsocket AND + com.github.jnr:jnr-enxio Necessary for Postgres support: org.postgresql:postgresql @@ -583,7 +584,7 @@ junit:junit,com.google.truth:truth,org.bouncycastle:bcpkix-jdk15on com.google.cloud.sql:jdbc-socket-factory-core:test-jar --> - org.ow2.asm:asm-util,com.github.jnr:jnr-unixsocket,org.postgresql:postgresql,junit:junit,com.google.truth:truth,com.microsoft.sqlserver:mssql-jdbc,com.google.guava:guava,org.bouncycastle:bcpkix-jdk15on,com.google.cloud.sql:jdbc-socket-factory-core:test-jar,com.google.auth:google-auth-library-credentials + org.ow2.asm:asm-util,com.github.jnr:jnr-unixsocket,com.github.jnr:jnr-enxio,org.postgresql:postgresql,junit:junit,com.google.truth:truth,com.microsoft.sqlserver:mssql-jdbc,com.google.guava:guava,org.bouncycastle:bcpkix-jdk15on,com.google.cloud.sql:jdbc-socket-factory-core:test-jar,com.google.auth:google-auth-library-credentials