diff --git a/oraclecloud-httpclient-netty/src/main/java/io/micronaut/oraclecloud/httpclient/netty/DomainSocketCustomizer.java b/oraclecloud-httpclient-netty/src/main/java/io/micronaut/oraclecloud/httpclient/netty/DomainSocketCustomizer.java new file mode 100644 index 000000000..a89536f34 --- /dev/null +++ b/oraclecloud-httpclient-netty/src/main/java/io/micronaut/oraclecloud/httpclient/netty/DomainSocketCustomizer.java @@ -0,0 +1,55 @@ +/* + * Copyright 2017-2024 original 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 + * + * https://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.micronaut.oraclecloud.httpclient.netty; + +import io.micronaut.context.annotation.Requires; +import io.micronaut.context.event.BeanCreatedEvent; +import io.micronaut.context.event.BeanCreatedEventListener; +import io.micronaut.core.annotation.NonNull; +import io.micronaut.http.client.netty.NettyClientCustomizer; +import io.netty.bootstrap.Bootstrap; +import jakarta.inject.Singleton; + +import java.net.UnixDomainSocketAddress; + +/** + * Customizer that replaces the remote address for proxying through a domain socket. + * + * @author Jonas Konrad + * @since 4.3.0 + */ +@Singleton +@Requires(property = OciNettyConfiguration.PREFIX + ".proxy-domain-socket") +final class DomainSocketCustomizer implements BeanCreatedEventListener { + private final OciNettyConfiguration configuration; + + DomainSocketCustomizer(OciNettyConfiguration configuration) { + this.configuration = configuration; + } + + @Override + public NettyClientCustomizer.Registry onCreated(@NonNull BeanCreatedEvent event) { + UnixDomainSocketAddress address = UnixDomainSocketAddress.of(configuration.proxyDomainSocket()); + event.getBean().register(new NettyClientCustomizer() { + @Override + public @NonNull NettyClientCustomizer specializeForBootstrap(@NonNull Bootstrap bootstrap) { + bootstrap.remoteAddress(address); + return this; + } + }); + return event.getBean(); + } +} diff --git a/oraclecloud-httpclient-netty/src/main/java/io/micronaut/oraclecloud/httpclient/netty/ManagedNettyHttpProvider.java b/oraclecloud-httpclient-netty/src/main/java/io/micronaut/oraclecloud/httpclient/netty/ManagedNettyHttpProvider.java index 55f9268fe..9a2db93a8 100644 --- a/oraclecloud-httpclient-netty/src/main/java/io/micronaut/oraclecloud/httpclient/netty/ManagedNettyHttpProvider.java +++ b/oraclecloud-httpclient-netty/src/main/java/io/micronaut/oraclecloud/httpclient/netty/ManagedNettyHttpProvider.java @@ -60,21 +60,24 @@ public class ManagedNettyHttpProvider implements HttpProvider { @Nullable final ExecutorService ioExecutor; final JsonMapper jsonMapper; + final OciNettyConfiguration configuration; @Inject - public ManagedNettyHttpProvider( + ManagedNettyHttpProvider( HttpClientRegistry mnHttpClientRegistry, @Named(TaskExecutors.BLOCKING) @Nullable ExecutorService ioExecutor, ObjectMapper jsonMapper, OciSerdeConfiguration ociSerdeConfiguration, OciSerializationConfiguration ociSerializationConfiguration, - @Nullable List> nettyClientFilters + @Nullable List> nettyClientFilters, + OciNettyConfiguration configuration ) { this.mnHttpClientRegistry = mnHttpClientRegistry; this.mnHttpClient = null; this.ioExecutor = ioExecutor; this.jsonMapper = jsonMapper.cloneWithConfiguration(ociSerdeConfiguration, ociSerializationConfiguration, null); this.nettyClientFilters = nettyClientFilters == null ? Collections.emptyList() : nettyClientFilters; + this.configuration = configuration; } // for OKE @@ -88,6 +91,7 @@ public ManagedNettyHttpProvider( this.ioExecutor = ioExecutor; this.jsonMapper = OciSdkMicronautSerializer.getDefaultObjectMapper(); this.nettyClientFilters = nettyClientFilters == null ? Collections.emptyList() : nettyClientFilters; + this.configuration = new OciNettyConfiguration(null); } @Override diff --git a/oraclecloud-httpclient-netty/src/main/java/io/micronaut/oraclecloud/httpclient/netty/NettyHttpClient.java b/oraclecloud-httpclient-netty/src/main/java/io/micronaut/oraclecloud/httpclient/netty/NettyHttpClient.java index f58502670..e64f7a39b 100644 --- a/oraclecloud-httpclient-netty/src/main/java/io/micronaut/oraclecloud/httpclient/netty/NettyHttpClient.java +++ b/oraclecloud-httpclient-netty/src/main/java/io/micronaut/oraclecloud/httpclient/netty/NettyHttpClient.java @@ -38,6 +38,7 @@ import java.io.IOException; import java.io.UncheckedIOException; import java.net.URI; +import java.net.URISyntaxException; import java.time.Duration; import java.util.Collections; import java.util.Comparator; @@ -59,6 +60,7 @@ final class NettyHttpClient implements HttpClient { final boolean hasContext; final boolean ownsThreadPool; + final boolean proxyDomainSocket; final URI baseUri; final List requestInterceptors; final List> nettyClientFilter; @@ -83,6 +85,7 @@ final class NettyHttpClient implements HttpClient { if (builder.managedProvider == null) { hasContext = false; ownsThreadPool = true; + proxyDomainSocket = false; DefaultHttpClientConfiguration cfg = new DefaultHttpClientConfiguration(); if (builder.properties.containsKey(StandardClientProperties.CONNECT_TIMEOUT)) { cfg.setConnectTimeout((Duration) builder.properties.get(StandardClientProperties.CONNECT_TIMEOUT)); @@ -117,10 +120,11 @@ final class NettyHttpClient implements HttpClient { blockingIoExecutor = builder.managedProvider.ioExecutor; } jsonMapper = builder.managedProvider.jsonMapper; + proxyDomainSocket = builder.managedProvider.configuration.proxyDomainSocket() != null; } upstreamHttpClient = mnClient; connectionManager = mnClient.connectionManager(); - baseUri = Objects.requireNonNull(builder.baseUri, "baseUri"); + this.baseUri = Objects.requireNonNull(builder.baseUri, "baseUri"); requestInterceptors = builder.requestInterceptors.stream() .sorted(Comparator.comparingInt(p -> p.priority)) .map(p -> p.value) @@ -132,7 +136,16 @@ final class NettyHttpClient implements HttpClient { nettyClientFilter = Collections.emptyList(); } - requestKey = new DefaultHttpClient.RequestKey(mnClient, this.baseUri); + URI baseUriForRequestKey = builder.baseUri; + if (proxyDomainSocket && baseUriForRequestKey.getScheme().equals("https")) { + // use a normal HTTP connection to the domain socket proxy. + try { + baseUriForRequestKey = new URI("http", baseUriForRequestKey.getSchemeSpecificPart(), baseUriForRequestKey.getFragment()); + } catch (URISyntaxException e) { + throw new RuntimeException(e); + } + } + requestKey = new DefaultHttpClient.RequestKey(mnClient, baseUriForRequestKey); this.buffered = builder.buffered; } diff --git a/oraclecloud-httpclient-netty/src/main/java/io/micronaut/oraclecloud/httpclient/netty/NettyHttpRequest.java b/oraclecloud-httpclient-netty/src/main/java/io/micronaut/oraclecloud/httpclient/netty/NettyHttpRequest.java index 25f916eed..5803b23bb 100644 --- a/oraclecloud-httpclient-netty/src/main/java/io/micronaut/oraclecloud/httpclient/netty/NettyHttpRequest.java +++ b/oraclecloud-httpclient-netty/src/main/java/io/micronaut/oraclecloud/httpclient/netty/NettyHttpRequest.java @@ -290,7 +290,7 @@ public CompletionStage execute() { } }, result::completeExceptionally); - return result; + return last; } private void bufferBody() { @@ -342,9 +342,15 @@ private io.netty.handler.codec.http.HttpRequest buildNettyRequest(ConnectionMana } } - String pathAndQuery = uri.getRawPath(); - if (uri.getRawQuery() != null) { - pathAndQuery = pathAndQuery + "?" + uri.getRawQuery(); + String pathAndQuery; + if (client.proxyDomainSocket) { + // when proxying, we need to include the full uri + pathAndQuery = uri.toASCIIString(); + } else { + pathAndQuery = uri.getRawPath(); + if (uri.getRawQuery() != null) { + pathAndQuery = pathAndQuery + "?" + uri.getRawQuery(); + } } boolean hasTransferHeader = headers.contains(HttpHeaderNames.CONTENT_LENGTH) || diff --git a/oraclecloud-httpclient-netty/src/main/java/io/micronaut/oraclecloud/httpclient/netty/OciNettyConfiguration.java b/oraclecloud-httpclient-netty/src/main/java/io/micronaut/oraclecloud/httpclient/netty/OciNettyConfiguration.java new file mode 100644 index 000000000..f6897d9fd --- /dev/null +++ b/oraclecloud-httpclient-netty/src/main/java/io/micronaut/oraclecloud/httpclient/netty/OciNettyConfiguration.java @@ -0,0 +1,39 @@ +/* + * Copyright 2017-2024 original 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 + * + * https://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.micronaut.oraclecloud.httpclient.netty; + +import io.micronaut.context.annotation.ConfigurationProperties; +import io.micronaut.core.annotation.Experimental; +import io.micronaut.core.annotation.Nullable; + +import java.nio.file.Path; + +/** + * Configuration properties specific to the managed client. + * + * @param proxyDomainSocket A domain socket to send all requests through. The requests will be sent + * as HTTP (no TLS) and with absolute-form URIs. + * @author Jonas Konrad + * @since 4.3.0 + */ +@ConfigurationProperties(OciNettyConfiguration.PREFIX) +record OciNettyConfiguration( + @Experimental + @Nullable + Path proxyDomainSocket +) { + static final String PREFIX = "oci.netty"; +} diff --git a/oraclecloud-httpclient-netty/src/main/java/io/micronaut/oraclecloud/httpclient/netty/UnixGroupFactory.java b/oraclecloud-httpclient-netty/src/main/java/io/micronaut/oraclecloud/httpclient/netty/UnixGroupFactory.java new file mode 100644 index 000000000..825899cdb --- /dev/null +++ b/oraclecloud-httpclient-netty/src/main/java/io/micronaut/oraclecloud/httpclient/netty/UnixGroupFactory.java @@ -0,0 +1,54 @@ +/* + * Copyright 2017-2024 original 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 + * + * https://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.micronaut.oraclecloud.httpclient.netty; + +import io.micronaut.context.annotation.Primary; +import io.micronaut.context.annotation.Replaces; +import io.micronaut.context.annotation.Requires; +import io.micronaut.core.annotation.Nullable; +import io.micronaut.http.netty.channel.DefaultEventLoopGroupFactory; +import io.micronaut.http.netty.channel.EventLoopGroupConfiguration; +import io.micronaut.http.netty.channel.EventLoopGroupFactory; +import io.micronaut.http.netty.channel.NettyChannelType; +import io.micronaut.http.netty.channel.NioEventLoopGroupFactory; +import io.micronaut.http.netty.configuration.NettyGlobalConfiguration; +import io.netty.channel.Channel; +import jakarta.inject.Singleton; + +/** + * {@link EventLoopGroupFactory} that makes the HTTP client use domain socket channels instead of + * normal nio channels. + * + * @author Jonas Konrad + * @since 4.3.0 + */ +@Primary +@Singleton +@Replaces(DefaultEventLoopGroupFactory.class) +@Requires(property = OciNettyConfiguration.PREFIX + ".proxy-domain-socket") +final class UnixGroupFactory extends DefaultEventLoopGroupFactory { + public UnixGroupFactory(NioEventLoopGroupFactory nioEventLoopGroupFactory, @Nullable EventLoopGroupFactory nativeFactory, @Nullable NettyGlobalConfiguration nettyGlobalConfiguration) { + super(nioEventLoopGroupFactory, nativeFactory, nettyGlobalConfiguration); + } + + @Override + public Channel channelInstance(NettyChannelType type, @Nullable EventLoopGroupConfiguration configuration) { + if (type == NettyChannelType.CLIENT_SOCKET) { + type = NettyChannelType.DOMAIN_SOCKET; + } + return super.channelInstance(type, configuration); + } +} diff --git a/oraclecloud-httpclient-netty/src/test/groovy/io/micronaut/oraclecloud/factory/ObjectStorageFactorySpec.groovy b/oraclecloud-httpclient-netty/src/test/groovy/io/micronaut/oraclecloud/factory/ObjectStorageFactorySpec.groovy index 0da874533..075ccca1d 100644 --- a/oraclecloud-httpclient-netty/src/test/groovy/io/micronaut/oraclecloud/factory/ObjectStorageFactorySpec.groovy +++ b/oraclecloud-httpclient-netty/src/test/groovy/io/micronaut/oraclecloud/factory/ObjectStorageFactorySpec.groovy @@ -2,6 +2,7 @@ package io.micronaut.oraclecloud.factory; import com.oracle.bmc.objectstorage.ObjectStorageAsyncClient import com.oracle.bmc.objectstorage.ObjectStorageClient +import io.micronaut.context.annotation.Requires import io.micronaut.context.event.BeanCreatedEvent import io.micronaut.context.event.BeanCreatedEventListener import io.micronaut.core.annotation.NonNull @@ -42,6 +43,7 @@ class ObjectStorageFactorySpec extends Specification { } @Singleton + @Requires(property = "spec.name", notEquals = "DomainSocketProxyTest") static class DatabaseClientBuilderListener implements BeanCreatedEventListener { @Override @@ -55,6 +57,7 @@ class ObjectStorageFactorySpec extends Specification { } @Singleton + @Requires(property = "spec.name", notEquals = "DomainSocketProxyTest") static class DatabaseAsyncClientBuilderListener implements BeanCreatedEventListener { @Override diff --git a/oraclecloud-httpclient-netty/src/test/java/io/micronaut/oraclecloud/httpclient/netty/DomainSocketProxyTest.java b/oraclecloud-httpclient-netty/src/test/java/io/micronaut/oraclecloud/httpclient/netty/DomainSocketProxyTest.java new file mode 100644 index 000000000..1d81507fd --- /dev/null +++ b/oraclecloud-httpclient-netty/src/test/java/io/micronaut/oraclecloud/httpclient/netty/DomainSocketProxyTest.java @@ -0,0 +1,135 @@ +package io.micronaut.oraclecloud.httpclient.netty; + +import com.oracle.bmc.Region; +import com.oracle.bmc.Service; +import com.oracle.bmc.auth.AbstractAuthenticationDetailsProvider; +import com.oracle.bmc.auth.ConfigFileAuthenticationDetailsProvider; +import com.oracle.bmc.auth.RegionProvider; +import com.oracle.bmc.http.signing.RequestSigner; +import com.oracle.bmc.http.signing.RequestSignerFactory; +import com.oracle.bmc.objectstorage.ObjectStorageClient; +import com.oracle.bmc.objectstorage.requests.ListBucketsRequest; +import io.micronaut.context.ApplicationContext; +import io.micronaut.context.annotation.Property; +import io.micronaut.context.annotation.Replaces; +import io.micronaut.context.annotation.Requires; +import io.micronaut.core.annotation.Internal; +import io.micronaut.core.annotation.NonNull; +import io.micronaut.http.netty.channel.EventLoopGroupRegistry; +import io.netty.bootstrap.ServerBootstrap; +import io.netty.buffer.Unpooled; +import io.netty.channel.Channel; +import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.ChannelInboundHandlerAdapter; +import io.netty.channel.ChannelInitializer; +import io.netty.channel.socket.nio.NioServerDomainSocketChannel; +import io.netty.handler.codec.http.DefaultFullHttpResponse; +import io.netty.handler.codec.http.FullHttpRequest; +import io.netty.handler.codec.http.HttpHeaderNames; +import io.netty.handler.codec.http.HttpMethod; +import io.netty.handler.codec.http.HttpObjectAggregator; +import io.netty.handler.codec.http.HttpResponseStatus; +import io.netty.handler.codec.http.HttpServerCodec; +import io.netty.handler.codec.http.HttpVersion; +import jakarta.inject.Singleton; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +import java.net.URI; +import java.net.UnixDomainSocketAddress; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.List; +import java.util.Map; + +public class DomainSocketProxyTest { + @Test + public void test() throws Exception { + Path tmpDir = Files.createTempDirectory("DomainSocketProxyTest"); + Path socketFile = tmpDir.resolve("sock"); + try (ApplicationContext ctx = ApplicationContext.run(Map.of( + "spec.name", "DomainSocketProxyTest", + "oci.region", Region.EU_FRANKFURT_1, + "oci.netty.proxy-domain-socket", socketFile + ))) { + new ServerBootstrap() + .group(ctx.getBean(EventLoopGroupRegistry.class).getDefaultEventLoopGroup(), ctx.getBean(EventLoopGroupRegistry.class).getDefaultEventLoopGroup()) + .channel(NioServerDomainSocketChannel.class) + .childHandler(new ChannelInitializer() { + @Override + protected void initChannel(@NotNull Channel ch) throws Exception { + ch.config().setAutoRead(true); + ch.pipeline() + .addLast(new HttpServerCodec()) + .addLast(new HttpObjectAggregator(8192)) + .addLast(new ChannelInboundHandlerAdapter() { + @Override + public void channelRead(@NotNull ChannelHandlerContext ctx, @NotNull Object msg) throws Exception { + System.out.println(msg); + + FullHttpRequest request = (FullHttpRequest) msg; + + Assertions.assertEquals(HttpMethod.GET, request.method()); + Assertions.assertEquals("https://objectstorage.eu-frankfurt-1.oraclecloud.com/n/namespaceName/b?compartmentId=compartmentId", request.uri()); + + request.release(); + + DefaultFullHttpResponse response = new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.OK, Unpooled.copiedBuffer("[]", StandardCharsets.UTF_8)); + response.headers().add(HttpHeaderNames.CONTENT_LENGTH, response.content().readableBytes()); + ctx.writeAndFlush(response); + } + }); + } + }) + .bind(UnixDomainSocketAddress.of(socketFile)).sync(); + + ctx.getBean(ObjectStorageClient.class).listBuckets(ListBucketsRequest.builder() + .compartmentId("compartmentId") + .namespaceName("namespaceName") + .build()); + } finally { + Files.deleteIfExists(socketFile); + Files.deleteIfExists(tmpDir); + } + } + + + @Singleton + @Internal + @Requires(property = "spec.name", value = "DomainSocketProxyTest") + @Requires(property = "oci.netty.proxy-domain-socket") + static class MockRequestSigner implements RequestSignerFactory { + @Override + public RequestSigner createRequestSigner(Service service, AbstractAuthenticationDetailsProvider abstractAuthProvider) { + return new RequestSigner() { + @Override + public @NotNull Map signRequest(@NotNull URI uri, @NotNull String httpMethod, @NotNull Map> headers, @Nullable Object body) { + return Map.of(); + } + }; + } + } + + @Singleton + @Internal + @Replaces(ConfigFileAuthenticationDetailsProvider.class) + @Requires(property = "spec.name", value = "DomainSocketProxyTest") + @Requires(property = "oci.netty.proxy-domain-socket") + static class NoOpAuthDetailsProvider implements AbstractAuthenticationDetailsProvider, RegionProvider { + + private final Region region; + + NoOpAuthDetailsProvider(@NonNull @Property(name = "oci.region") String regionId) { + this.region = Region.fromRegionId(regionId); + } + + @Override + public Region getRegion() { + return region; + } + + } +} diff --git a/oraclecloud-httpclient-netty/src/test/java/io/micronaut/oraclecloud/httpclient/netty/ManagedTest.java b/oraclecloud-httpclient-netty/src/test/java/io/micronaut/oraclecloud/httpclient/netty/ManagedTest.java index 18238b7a2..ffd51267f 100644 --- a/oraclecloud-httpclient-netty/src/test/java/io/micronaut/oraclecloud/httpclient/netty/ManagedTest.java +++ b/oraclecloud-httpclient-netty/src/test/java/io/micronaut/oraclecloud/httpclient/netty/ManagedTest.java @@ -50,12 +50,13 @@ public void managedClientUsesManagedProvider() { public static class MockProvider extends ManagedNettyHttpProvider { int buildersCreated = 0; - public MockProvider( + MockProvider( HttpClientRegistry mnHttpClientRegistry, @Named(TaskExecutors.BLOCKING) ExecutorService ioExecutor, ObjectMapper jsonMapper, OciSerdeConfiguration ociSerdeConfiguration, OciSerializationConfiguration ociSerializationConfiguration, - List> nettyClientFilters + List> nettyClientFilters, + OciNettyConfiguration configuration ) { - super(mnHttpClientRegistry, ioExecutor, jsonMapper, ociSerdeConfiguration, ociSerializationConfiguration, nettyClientFilters); + super(mnHttpClientRegistry, ioExecutor, jsonMapper, ociSerdeConfiguration, ociSerializationConfiguration, nettyClientFilters, configuration); } @Override diff --git a/oraclecloud-httpclient-netty/src/test/java/io/micronaut/oraclecloud/httpclient/netty/MockAuthenticationDetailsProvider.java b/oraclecloud-httpclient-netty/src/test/java/io/micronaut/oraclecloud/httpclient/netty/MockAuthenticationDetailsProvider.java index 37acf867b..9ad0340f1 100644 --- a/oraclecloud-httpclient-netty/src/test/java/io/micronaut/oraclecloud/httpclient/netty/MockAuthenticationDetailsProvider.java +++ b/oraclecloud-httpclient-netty/src/test/java/io/micronaut/oraclecloud/httpclient/netty/MockAuthenticationDetailsProvider.java @@ -4,6 +4,7 @@ import com.oracle.bmc.auth.BasicAuthenticationDetailsProvider; import com.oracle.bmc.auth.ConfigFileAuthenticationDetailsProvider; import io.micronaut.context.annotation.Replaces; +import io.micronaut.context.annotation.Requires; import jakarta.inject.Singleton; import java.io.InputStream; @@ -11,6 +12,7 @@ @AuthCachingPolicy(cacheKeyId = false, cachePrivateKey = false) @Singleton @Replaces(ConfigFileAuthenticationDetailsProvider.class) +@Requires(property = "spec.name", notEquals = "DomainSocketProxyTest") public class MockAuthenticationDetailsProvider implements BasicAuthenticationDetailsProvider { @Override