michael-o commented on code in PR #637: URL: https://github.com/apache/httpcomponents-client/pull/637#discussion_r2120917670
########## httpclient5/src/test/java/org/apache/hc/client5/http/TestHttpRoute.java: ########## @@ -454,4 +458,44 @@ void testImmutable() throws CloneNotSupportedException { Assertions.assertEquals(route3, route1, "route was modified"); } + @Test + void testUnixDomainSocketModeling() { + final Path uds1 = (new File("/var/run/docker.sock")).toPath(); Review Comment: Why not create a `Path` directly? ########## pom.xml: ########## @@ -163,6 +164,12 @@ <version>${rxjava.version}</version> <scope>test</scope> </dependency> + <dependency> + <groupId>com.kohlschutter.junixsocket</groupId> + <artifactId>junixsocket-core</artifactId> + <version>${junixsocket.version}</version> + <type>pom</type> Review Comment: So this will pull all deps? Is this not optional? Not everyone needs UDS support. ########## httpclient5/src/test/java/org/apache/hc/client5/http/examples/UnixDomainSocketAsync.java: ########## @@ -0,0 +1,93 @@ +/* + * ==================================================================== + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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. + * ==================================================================== + * + * This software consists of voluntary contributions made by many + * individuals on behalf of the Apache Software Foundation. For more + * information on the Apache Software Foundation, please see + * <http://www.apache.org/>. + * + */ +package org.apache.hc.client5.http.examples; + +import io.reactivex.Observable; +import org.apache.hc.client5.http.async.methods.SimpleHttpRequest; +import org.apache.hc.client5.http.async.methods.SimpleRequestProducer; +import org.apache.hc.client5.http.config.RequestConfig; +import org.apache.hc.client5.http.impl.async.CloseableHttpAsyncClient; +import org.apache.hc.client5.http.impl.async.HttpAsyncClientBuilder; +import org.apache.hc.core5.http.HttpResponse; +import org.apache.hc.core5.http.Message; +import org.apache.hc.core5.http.Method; +import org.apache.hc.core5.reactive.ReactiveResponseConsumer; +import org.reactivestreams.Publisher; + +import java.io.File; +import java.io.PrintStream; +import java.nio.ByteBuffer; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.TimeUnit; + +public class UnixDomainSocketAsync { + public static void main(final String[] args) throws Exception { + if (args.length == 0 || "-h".equals(args[0]) || "--help".equals(args[0])) { + usage(System.out); + return; + } else if (args.length != 2) { + usage(System.err); + return; + } + + final Path unixDomainSocket = new File(args[0]).toPath(); + final String uri = args[1]; + + final RequestConfig requestConfig = RequestConfig.custom().setUnixDomainSocket(unixDomainSocket).build(); + try (CloseableHttpAsyncClient client = HttpAsyncClientBuilder.create() + .setDefaultRequestConfig(requestConfig) + .build()) { + client.start(); + + final SimpleHttpRequest httpGet = SimpleHttpRequest.create(Method.GET.name(), uri); + httpGet.setConfig(requestConfig); + + final ReactiveResponseConsumer consumer = new ReactiveResponseConsumer(); + client.execute(SimpleRequestProducer.create(httpGet), consumer, null).get(10, TimeUnit.SECONDS); + final Message<HttpResponse, Publisher<ByteBuffer>> message = consumer.getResponseFuture() + .get(10, TimeUnit.SECONDS); + final List<ByteBuffer> bufs = Observable.fromPublisher(message.getBody()) + .collectInto(new ArrayList<ByteBuffer>(), List::add) + .blockingGet(); + for (final ByteBuffer buf : bufs) { + final byte[] bytes = new byte[buf.remaining()]; + buf.get(bytes); + System.out.write(bytes); + } + } + } + + private static void usage(final PrintStream printStream) { + printStream.println("Usage: UnixDomainSocketAsync [path] [uri]"); + printStream.println(); + printStream.println("Examples:"); + printStream.println("UnixDomainSocketAsync /var/run/docker.sock 'http://localhost/info'"); Review Comment: ditto ########## httpclient5/src/test/java/org/apache/hc/client5/http/examples/UnixDomainSocket.java: ########## @@ -0,0 +1,75 @@ +/* + * ==================================================================== + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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. + * ==================================================================== + * + * This software consists of voluntary contributions made by many + * individuals on behalf of the Apache Software Foundation. For more + * information on the Apache Software Foundation, please see + * <http://www.apache.org/>. + * + */ +package org.apache.hc.client5.http.examples; + +import org.apache.hc.client5.http.classic.methods.HttpGet; +import org.apache.hc.client5.http.config.RequestConfig; +import org.apache.hc.client5.http.impl.classic.CloseableHttpClient; +import org.apache.hc.client5.http.impl.classic.HttpClientBuilder; + +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.io.PrintStream; +import java.nio.file.Path; + +public class UnixDomainSocket { + public static void main(final String[] args) throws IOException { + if (args.length == 0 || "-h".equals(args[0]) || "--help".equals(args[0])) { + usage(System.out); + return; + } else if (args.length != 2) { + usage(System.err); + return; + } + + final Path unixDomainSocket = new File(args[0]).toPath(); + final String uri = args[1]; + + try (CloseableHttpClient client = HttpClientBuilder.create().build()) { + final HttpGet httpGet = new HttpGet(uri); + httpGet.setConfig(RequestConfig.custom().setUnixDomainSocket(unixDomainSocket).build()); + client.execute(httpGet, classicHttpResponse -> { + final InputStream inputStream = classicHttpResponse.getEntity().getContent(); + final byte[] buf = new byte[8192]; + int len; + while ((len = inputStream.read(buf)) > 0) { + System.out.write(buf, 0, len); + } + return null; + }); + } + } + + private static void usage(final PrintStream printStream) { + printStream.println("Usage: UnixDomainSocket [path] [uri]"); + printStream.println(); + printStream.println("Examples:"); + printStream.println("UnixDomainSocket /var/run/docker.sock 'http://localhost/info'"); Review Comment: Rather use `/tmp/foo.sock` or similar since there is no guarantee that the using running this has write permissions to `/var/run`. ########## httpclient5/src/main/java/org/apache/hc/client5/http/impl/nio/DefaultAsyncClientConnectionOperator.java: ########## @@ -180,6 +191,18 @@ public void cancelled() { return future; } + // The IOReactor does not support AFUNIXSocketChannel from JUnixSocket, so if a Unix domain socket was configured, + // we must use JEP 380 sockets and addresses. + private static SocketAddress createUnixSocketAddress(final Path socketPath) { + try { + final Class<?> addressClass = Class.forName("java.net.UnixDomainSocketAddress"); + final Method ofMethod = addressClass.getMethod("of", Path.class); + return (SocketAddress) ofMethod.invoke(null, socketPath); + } catch (final ReflectiveOperationException ex) { + throw new UnsupportedOperationException("Async Unix domain socket support requires JDK16 or later", ex); Review Comment: Java 16, not JDK. You can also run a JRE image only. ########## httpclient5/src/main/java/org/apache/hc/client5/http/socket/UnixDomainSocketFactory.java: ########## @@ -0,0 +1,179 @@ +/* + * ==================================================================== + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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. + * ==================================================================== + * + * This software consists of voluntary contributions made by many + * individuals on behalf of the Apache Software Foundation. For more + * information on the Apache Software Foundation, please see + * <http://www.apache.org/>. + * + */ + +package org.apache.hc.client5.http.socket; + +import org.apache.hc.core5.annotation.Contract; +import org.apache.hc.core5.annotation.Internal; +import org.apache.hc.core5.annotation.ThreadingBehavior; +import org.apache.hc.core5.util.Args; +import org.apache.hc.core5.util.TimeValue; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.IOException; +import java.lang.reflect.Method; +import java.net.Socket; +import java.net.SocketAddress; +import java.nio.file.Path; + +/** + * A factory for Unix domain sockets. + * <p> + * This implementation supports both the JDK16+ standard library implementation (JEP 380) and the JUnixSocket library. + * It will automatically detect which implementation is available and use it. JUnixSocket is preferred, since it + * supports the {@link java.net.Socket} API used by the classic client. + * </p> + * + * @since 5.6 + */ +@Contract(threading = ThreadingBehavior.STATELESS) +@Internal +public final class UnixDomainSocketFactory { + private static final Logger LOG = LoggerFactory.getLogger(UnixDomainSocketFactory.class); + + private static final String JDK_UNIX_SOCKET_ADDRESS_CLASS = "java.net.UnixDomainSocketAddress"; + private static final String JUNIXSOCKET_SOCKET_CLASS = "org.newsclub.net.unix.AFUNIXSocket"; + private static final String JUNIXSOCKET_ADDRESS_CLASS = "org.newsclub.net.unix.AFUNIXSocketAddress"; + + private enum Implementation { + JDK, + JUNIXSOCKET, + NONE + } + + private static final Implementation IMPLEMENTATION = detectImplementation(); + + private static Implementation detectImplementation() { + try { + Class.forName(JUNIXSOCKET_SOCKET_CLASS); + LOG.debug("Using JUnixSocket Unix Domain Socket implementation"); + return Implementation.JUNIXSOCKET; + } catch (final ClassNotFoundException e) { + try { + Class.forName(JDK_UNIX_SOCKET_ADDRESS_CLASS); + LOG.debug("Using JDK Unix Domain Socket implementation"); + return Implementation.JDK; + } catch (final ClassNotFoundException e2) { + LOG.debug("No Unix Domain Socket implementation found"); + return Implementation.NONE; + } + } + } + + /** + * Checks if Unix Domain Socket support is available. + * + * @return true if Unix Domain Socket support is available, false otherwise + */ + public static boolean isAvailable() { + return IMPLEMENTATION != Implementation.NONE; + } + + /** + * Default instance of {@link UnixDomainSocketFactory}. + */ + private static final UnixDomainSocketFactory INSTANCE = new UnixDomainSocketFactory(); + + /** + * Gets the singleton instance of {@link UnixDomainSocketFactory}. + * + * @return the singleton instance + */ + public static UnixDomainSocketFactory getSocketFactory() { + return INSTANCE; + } + + public SocketAddress createSocketAddress(final Path socketPath) { + if (!isAvailable()) { + throw new UnsupportedOperationException("Unix Domain Socket support is not available"); + } + Args.notNull(socketPath, "Unix domain socket path"); + + try { + if (IMPLEMENTATION == Implementation.JDK) { + // JDK implementation + final Class<?> addressClass = Class.forName(JDK_UNIX_SOCKET_ADDRESS_CLASS); + final Method ofMethod = addressClass.getMethod("of", Path.class); + return (SocketAddress) ofMethod.invoke(null, socketPath); + } else { + // JUnixSocket implementation + final Class<?> addressClass = Class.forName(JUNIXSOCKET_ADDRESS_CLASS); + final Method ofMethod = addressClass.getMethod("of", Path.class); + return (SocketAddress) ofMethod.invoke(null, socketPath); + } + } catch (final ReflectiveOperationException ex) { + throw new RuntimeException("Could not create UDS SocketAddress", ex); + } + } + + public Socket createSocket() throws IOException { + if (!isAvailable()) { + throw new UnsupportedOperationException("Unix Domain Socket support is not available"); + } + + try { + if (IMPLEMENTATION == Implementation.JDK) { + // JDK16 only supports UDS through the SocketChannel API, but the sync client is coupled + // to the legacy Socket API. In order to use JDK sockets, we first need to write an Review Comment: ditto ########## httpclient5/src/test/java/org/apache/hc/client5/http/TestHttpRoute.java: ########## @@ -454,4 +458,44 @@ void testImmutable() throws CloneNotSupportedException { Assertions.assertEquals(route3, route1, "route was modified"); } + @Test + void testUnixDomainSocketModeling() { + final Path uds1 = (new File("/var/run/docker.sock")).toPath(); Review Comment: Don't confuse people with `"docker`", use something neutral. ########## httpclient5/src/main/java/org/apache/hc/client5/http/socket/UnixDomainSocketFactory.java: ########## @@ -0,0 +1,179 @@ +/* + * ==================================================================== + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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. + * ==================================================================== + * + * This software consists of voluntary contributions made by many + * individuals on behalf of the Apache Software Foundation. For more + * information on the Apache Software Foundation, please see + * <http://www.apache.org/>. + * + */ + +package org.apache.hc.client5.http.socket; + +import org.apache.hc.core5.annotation.Contract; +import org.apache.hc.core5.annotation.Internal; +import org.apache.hc.core5.annotation.ThreadingBehavior; +import org.apache.hc.core5.util.Args; +import org.apache.hc.core5.util.TimeValue; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.IOException; +import java.lang.reflect.Method; +import java.net.Socket; +import java.net.SocketAddress; +import java.nio.file.Path; + +/** + * A factory for Unix domain sockets. + * <p> + * This implementation supports both the JDK16+ standard library implementation (JEP 380) and the JUnixSocket library. + * It will automatically detect which implementation is available and use it. JUnixSocket is preferred, since it + * supports the {@link java.net.Socket} API used by the classic client. + * </p> + * + * @since 5.6 + */ +@Contract(threading = ThreadingBehavior.STATELESS) +@Internal +public final class UnixDomainSocketFactory { + private static final Logger LOG = LoggerFactory.getLogger(UnixDomainSocketFactory.class); + + private static final String JDK_UNIX_SOCKET_ADDRESS_CLASS = "java.net.UnixDomainSocketAddress"; + private static final String JUNIXSOCKET_SOCKET_CLASS = "org.newsclub.net.unix.AFUNIXSocket"; + private static final String JUNIXSOCKET_ADDRESS_CLASS = "org.newsclub.net.unix.AFUNIXSocketAddress"; + + private enum Implementation { + JDK, + JUNIXSOCKET, + NONE + } + + private static final Implementation IMPLEMENTATION = detectImplementation(); + + private static Implementation detectImplementation() { + try { + Class.forName(JUNIXSOCKET_SOCKET_CLASS); + LOG.debug("Using JUnixSocket Unix Domain Socket implementation"); + return Implementation.JUNIXSOCKET; + } catch (final ClassNotFoundException e) { + try { + Class.forName(JDK_UNIX_SOCKET_ADDRESS_CLASS); + LOG.debug("Using JDK Unix Domain Socket implementation"); + return Implementation.JDK; + } catch (final ClassNotFoundException e2) { + LOG.debug("No Unix Domain Socket implementation found"); + return Implementation.NONE; + } + } + } + + /** + * Checks if Unix Domain Socket support is available. + * + * @return true if Unix Domain Socket support is available, false otherwise + */ + public static boolean isAvailable() { + return IMPLEMENTATION != Implementation.NONE; + } + + /** + * Default instance of {@link UnixDomainSocketFactory}. + */ + private static final UnixDomainSocketFactory INSTANCE = new UnixDomainSocketFactory(); + + /** + * Gets the singleton instance of {@link UnixDomainSocketFactory}. + * + * @return the singleton instance + */ + public static UnixDomainSocketFactory getSocketFactory() { + return INSTANCE; + } + + public SocketAddress createSocketAddress(final Path socketPath) { + if (!isAvailable()) { + throw new UnsupportedOperationException("Unix Domain Socket support is not available"); + } + Args.notNull(socketPath, "Unix domain socket path"); + + try { + if (IMPLEMENTATION == Implementation.JDK) { + // JDK implementation + final Class<?> addressClass = Class.forName(JDK_UNIX_SOCKET_ADDRESS_CLASS); + final Method ofMethod = addressClass.getMethod("of", Path.class); + return (SocketAddress) ofMethod.invoke(null, socketPath); + } else { + // JUnixSocket implementation + final Class<?> addressClass = Class.forName(JUNIXSOCKET_ADDRESS_CLASS); + final Method ofMethod = addressClass.getMethod("of", Path.class); + return (SocketAddress) ofMethod.invoke(null, socketPath); + } + } catch (final ReflectiveOperationException ex) { + throw new RuntimeException("Could not create UDS SocketAddress", ex); + } + } + + public Socket createSocket() throws IOException { + if (!isAvailable()) { + throw new UnsupportedOperationException("Unix Domain Socket support is not available"); + } + + try { + if (IMPLEMENTATION == Implementation.JDK) { + // JDK16 only supports UDS through the SocketChannel API, but the sync client is coupled Review Comment: ditto -- This is an automated message from the Apache Git Service. To respond to the message, please log on to GitHub and use the URL above to go to the specific comment. To unsubscribe, e-mail: dev-unsubscr...@hc.apache.org For queries about this service, please contact Infrastructure at: us...@infra.apache.org --------------------------------------------------------------------- To unsubscribe, e-mail: dev-unsubscr...@hc.apache.org For additional commands, e-mail: dev-h...@hc.apache.org