View Javadoc
1   /*
2    * junixsocket
3    *
4    * Copyright 2009-2026 Christian Kohlschütter
5    *
6    * Licensed under the Apache License, Version 2.0 (the "License");
7    * you may not use this file except in compliance with the License.
8    * You may obtain a copy of the License at
9    *
10   *     http://www.apache.org/licenses/LICENSE-2.0
11   *
12   * Unless required by applicable law or agreed to in writing, software
13   * distributed under the License is distributed on an "AS IS" BASIS,
14   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15   * See the License for the specific language governing permissions and
16   * limitations under the License.
17   */
18  package org.newsclub.net.unix;
19  
20  import java.io.File;
21  import java.io.FileNotFoundException;
22  import java.io.IOException;
23  import java.net.InetAddress;
24  import java.net.InetSocketAddress;
25  import java.net.SocketAddress;
26  import java.net.SocketException;
27  import java.net.URI;
28  import java.net.URISyntaxException;
29  import java.nio.ByteBuffer;
30  import java.nio.charset.Charset;
31  import java.nio.charset.StandardCharsets;
32  import java.nio.file.Path;
33  import java.util.Arrays;
34  import java.util.HashSet;
35  import java.util.Locale;
36  import java.util.Objects;
37  import java.util.Set;
38  
39  import org.eclipse.jdt.annotation.NonNull;
40  import org.newsclub.net.unix.pool.ObjectPool.Lease;
41  
42  import com.kohlschutter.annotations.compiletime.SuppressFBWarnings;
43  
44  /**
45   * Describes an {@link InetSocketAddress} that actually uses AF_UNIX sockets instead of AF_INET.
46   *
47   * The ability to specify a port number is not specified by AF_UNIX sockets, but we need it
48   * sometimes, for example for RMI-over-AF_UNIX.
49   *
50   * @author Christian Kohlschütter
51   */
52  @SuppressWarnings("PMD.ShortMethodName")
53  @SuppressFBWarnings("PATH_TRAVERSAL_IN")
54  public final class AFUNIXSocketAddress extends AFSocketAddress {
55    private static final long serialVersionUID = 1L; // do not change!
56  
57    private static final Charset ADDRESS_CHARSET = Charset.defaultCharset();
58  
59    @SuppressWarnings("null")
60    static final AFAddressFamily<@NonNull AFUNIXSocketAddress> AF_UNIX = AFAddressFamily
61        .registerAddressFamily("un", //
62            AFUNIXSocketAddress.class, new AFSocketAddressConfig<AFUNIXSocketAddress>() {
63  
64              private final AFSocketAddressConstructor<AFUNIXSocketAddress> addrConstr =
65                  isUseDeserializationForInit() ? AFUNIXSocketAddress::newAFSocketAddress
66                      : AFUNIXSocketAddress::new;
67  
68              @Override
69              public AFUNIXSocketAddress parseURI(URI u, int port) throws SocketException {
70                return AFUNIXSocketAddress.of(u, port);
71              }
72  
73              @Override
74              protected AFSocketAddressConstructor<AFUNIXSocketAddress> addressConstructor() {
75                return addrConstr;
76              }
77  
78              @Override
79              protected String selectorProviderClassname() {
80                return AFUNIXSelectorProvider.class.getName();
81              }
82  
83              @Override
84              protected Set<String> uriSchemes() {
85                return new HashSet<>(Arrays.asList("unix", "http+unix", "https+unix"));
86              }
87  
88              @Override
89              protected SocketAddress nullBindAddress() throws IOException {
90                return AFUNIXSocketAddress.ofNewTempFile();
91              }
92            });
93  
94    private AFUNIXSocketAddress(int port, final byte[] socketAddress, Lease<ByteBuffer> nativeAddress)
95        throws SocketException {
96      super(port, socketAddress, nativeAddress, AF_UNIX);
97    }
98  
99    /**
100    * Returns an {@link AFUNIXSocketAddress} that points to the AF_UNIX socket specified by the given
101    * file and port. <b>Legacy constructor, do not use!</b>
102    *
103    * @param socketFile The socket to connect to.
104    * @throws SocketException if the operation fails.
105    * @deprecated Use {@link #of(File)} instead.
106    * @see #of(File)
107    */
108   @Deprecated
109   public AFUNIXSocketAddress(File socketFile) throws SocketException {
110     this(socketFile, 0);
111   }
112 
113   /**
114    * Returns an {@link AFUNIXSocketAddress} that points to the AF_UNIX socket specified by the given
115    * file. <b>Legacy constructor, do not use!</b>
116    *
117    * @param socketFile The socket to connect to.
118    * @param port The port associated with this socket, or {@code 0} when no port should be assigned.
119    * @throws SocketException if the operation fails.
120    * @deprecated Use {@link #of(File, int)} instead.
121    * @see #of(File, int)
122    */
123   @Deprecated
124   public AFUNIXSocketAddress(File socketFile, int port) throws SocketException {
125     this(port, of(socketFile, port).getPathAsBytes(), of(socketFile, port)
126         .getNativeAddressDirectBuffer());
127   }
128 
129   static AFUNIXSocketAddress newAFSocketAddress(int port, final byte[] socketAddress,
130       Lease<ByteBuffer> nativeAddress) throws SocketException {
131     return newDeserializedAFSocketAddress(port, socketAddress, nativeAddress, AF_UNIX,
132         AFUNIXSocketAddress::new);
133   }
134 
135   /**
136    * Returns an {@link AFUNIXSocketAddress} that points to the AF_UNIX socket specified by the given
137    * file.
138    *
139    * @param socketFile The socket to connect to.
140    * @return A corresponding {@link AFUNIXSocketAddress} instance.
141    * @throws SocketException if the operation fails.
142    */
143   public static AFUNIXSocketAddress of(final File socketFile) throws SocketException {
144     return of(socketFile, 0);
145   }
146 
147   /**
148    * Returns an {@link AFUNIXSocketAddress} that points to the AF_UNIX socket specified by the given
149    * file, assigning the given port to it.
150    *
151    * @param socketFile The socket to connect to.
152    * @param port The port associated with this socket, or {@code 0} when no port should be assigned.
153    * @return A corresponding {@link AFUNIXSocketAddress} instance.
154    * @throws SocketException if the operation fails.
155    */
156   public static AFUNIXSocketAddress of(final File socketFile, int port) throws SocketException {
157     return of(socketFile.getPath().getBytes(ADDRESS_CHARSET), port);
158   }
159 
160   /**
161    * Returns an {@link AFUNIXSocketAddress} that points to the AF_UNIX socket specified by the given
162    * byte sequence.
163    *
164    * NOTE: By specifying a byte array that starts with a zero byte, you indicate that the abstract
165    * namespace is to be used. This feature is not available on all target platforms.
166    *
167    * @param socketAddress The socket address (as bytes).
168    * @return A corresponding {@link AFUNIXSocketAddress} instance.
169    * @throws SocketException if the operation fails.
170    * @see AFUNIXSocketAddress#inAbstractNamespace(String)
171    */
172   public static AFUNIXSocketAddress of(final byte[] socketAddress) throws SocketException {
173     return of(socketAddress, 0);
174   }
175 
176   /**
177    * Returns an {@link AFUNIXSocketAddress} that points to the AF_UNIX socket specified by the given
178    * byte sequence, assigning the given port to it.
179    *
180    * NOTE: By specifying a byte array that starts with a zero byte, you indicate that the abstract
181    * namespace is to be used. This feature is not available on all target platforms.
182    *
183    * @param socketAddress The socket address (as bytes).
184    * @param port The port associated with this socket, or {@code 0} when no port should be assigned.
185    * @return A corresponding {@link AFUNIXSocketAddress} instance.
186    * @throws SocketException if the operation fails.
187    * @see AFUNIXSocketAddress#inAbstractNamespace(String,int)
188    */
189   public static AFUNIXSocketAddress of(final byte[] socketAddress, int port)
190       throws SocketException {
191     return AFSocketAddress.resolveAddress(socketAddress, port, AF_UNIX);
192   }
193 
194   /**
195    * Returns an {@link AFUNIXSocketAddress} that points to the AF_UNIX socket specified by the given
196    * path.
197    *
198    * @param socketPath The socket to connect to.
199    * @return A corresponding {@link AFUNIXSocketAddress} instance.
200    * @throws SocketException if the operation fails.
201    */
202   public static AFUNIXSocketAddress of(Path socketPath) throws SocketException {
203     return of(socketPath, 0);
204   }
205 
206   /**
207    * Returns an {@link AFUNIXSocketAddress} that points to the AF_UNIX socket specified by the given
208    * path, assigning the given port to it.
209    *
210    * @param socketPath The socket to connect to.
211    * @param port The port associated with this socket, or {@code 0} when no port should be assigned.
212    * @return A corresponding {@link AFUNIXSocketAddress} instance.
213    * @throws SocketException if the operation fails.
214    */
215   public static AFUNIXSocketAddress of(Path socketPath, int port) throws SocketException {
216     if (!PathUtil.isPathInDefaultFileSystem(socketPath)) {
217       throw new SocketException("Path is not in the default file system");
218     }
219 
220     return of(socketPath.toString().getBytes(ADDRESS_CHARSET), port);
221   }
222 
223   /**
224    * Returns an {@link AFUNIXSocketAddress} for the given URI, if possible.
225    *
226    * @param u The URI.
227    * @return The address.
228    * @throws SocketException if the operation fails.
229    */
230   public static AFUNIXSocketAddress of(URI u) throws SocketException {
231     return of(u, -1);
232   }
233 
234   /**
235    * Returns an {@link AFUNIXSocketAddress} for the given URI, if possible.
236    *
237    * @param u The URI.
238    * @param overridePort The port to forcibly use, or {@code -1} for "don't override".
239    * @return The address.
240    * @throws SocketException if the operation fails.
241    */
242   public static AFUNIXSocketAddress of(URI u, int overridePort) throws SocketException {
243     switch (u.getScheme()) {
244       case "file":
245       case "unix":
246         String path = u.getPath();
247         if (path == null || path.isEmpty()) {
248           String auth = u.getAuthority();
249           if (auth != null && !auth.isEmpty() && u.getRawSchemeSpecificPart().indexOf('@') == -1) {
250             path = auth;
251           } else {
252             throw new SocketException("Cannot find UNIX socket path component from URI: " + u);
253           }
254         }
255         return of(new File(path), overridePort != -1 ? overridePort : u.getPort());
256       case "http+unix":
257       case "https+unix":
258         HostAndPort hp = HostAndPort.parseFrom(u);
259         return of(new File(hp.getHostname()), overridePort != -1 ? overridePort : hp.getPort());
260       default:
261         throw new SocketException("Invalid URI");
262     }
263   }
264 
265   /**
266    * Returns an {@link AFUNIXSocketAddress} that points to a temporary, non-existent but accessible
267    * path in the file system.
268    *
269    * @return A corresponding {@link AFUNIXSocketAddress} instance.
270    * @throws IOException if the operation fails.
271    */
272   public static AFUNIXSocketAddress ofNewTempFile() throws IOException {
273     return ofNewTempPath(0);
274   }
275 
276   /**
277    * Returns an {@link AFUNIXSocketAddress} that points to a temporary, non-existent but accessible
278    * path in the file system, assigning the given port to it.
279    *
280    * @param port The port associated with this socket, or {@code 0} when no port should be assigned.
281    * @return A corresponding {@link AFUNIXSocketAddress} instance.
282    * @throws IOException if the operation fails.
283    */
284   public static AFUNIXSocketAddress ofNewTempPath(int port) throws IOException {
285     return of(newTempPath(true), port);
286   }
287 
288   /**
289    * Returns an {@link AFUNIXSocketAddress} based on the given {@link SocketAddress}.
290    *
291    * This either simply casts an existing {@link AFUNIXSocketAddress}, or converts a
292    * {@code UnixDomainSocketAddress} to it.
293    *
294    * @param address The address to convert.
295    * @return A corresponding {@link AFUNIXSocketAddress} instance.
296    * @throws SocketException if the operation fails.
297    */
298   public static AFUNIXSocketAddress of(SocketAddress address) throws IOException {
299     AFUNIXSocketAddress addr = unwrap(Objects.requireNonNull(address));
300     if (addr == null) {
301       throw new SocketException("Could not convert SocketAddress to AFUNIXSocketAddress");
302     }
303     return addr;
304   }
305 
306   static File newTempPath(boolean deleteOnExit) throws IOException {
307     File f = TempFileUtil.INSTANCE.newPathForUnixDomainSocket(true).toFile();
308     if (deleteOnExit) {
309       f.deleteOnExit(); // always delete on exit to clean-up sockets created under that name
310     }
311     return f;
312   }
313 
314   /**
315    * Returns an {@link AFUNIXSocketAddress} given a special {@link InetAddress} that encodes the
316    * byte sequence of an AF_UNIX socket address, like those returned by {@link #wrapAddress()}.
317    *
318    * @param address The "special" {@link InetAddress}.
319    * @param port The port (use 0 for "none").
320    * @return The {@link AFUNIXSocketAddress} instance.
321    * @throws SocketException if the operation fails, for example when an unsupported address is
322    *           specified.
323    */
324   public static AFUNIXSocketAddress unwrap(InetAddress address, int port) throws SocketException {
325     return AFSocketAddress.unwrap(address, port, AF_UNIX);
326   }
327 
328   /**
329    * Returns an {@link AFUNIXSocketAddress} given a generic {@link SocketAddress}.
330    *
331    * @param address The address to unwrap.
332    * @return The {@link AFUNIXSocketAddress} instance.
333    * @throws SocketException if the operation fails, for example when an unsupported address is
334    *           specified.
335    */
336   public static AFUNIXSocketAddress unwrap(SocketAddress address) throws SocketException {
337     Objects.requireNonNull(address);
338     AFSupplier<AFUNIXSocketAddress> supplier = supportedAddressSupplier(address);
339     if (supplier == null) {
340       throw new SocketException("Unsupported address");
341     }
342     return supplier.get();
343   }
344 
345   /**
346    * Returns an {@link AFUNIXSocketAddress} given a special {@link InetAddress} hostname that
347    * encodes the byte sequence of an AF_UNIX socket address, like those returned by
348    * {@link #wrapAddress()}.
349    *
350    * @param hostname The "special" hostname, as provided by {@link InetAddress#getHostName()}.
351    * @param port The port (use 0 for "none").
352    * @return The {@link AFUNIXSocketAddress} instance.
353    * @throws SocketException if the operation fails, for example when an unsupported address is
354    *           specified.
355    */
356   public static AFUNIXSocketAddress unwrap(String hostname, int port) throws SocketException {
357     return AFSocketAddress.unwrap(hostname, port, AF_UNIX);
358   }
359 
360   /**
361    * Convenience method to create an {@link AFUNIXSocketAddress} in the abstract namespace.
362    *
363    * The returned socket address will use the byte representation of this identifier (using the
364    * system's default character encoding), prefixed with a null byte (to indicate the abstract
365    * namespace is used).
366    *
367    * @param name The identifier in the abstract namespace, without trailing zero or @.
368    * @return The address.
369    * @throws SocketException if the operation fails.
370    */
371   public static AFUNIXSocketAddress inAbstractNamespace(String name) throws SocketException {
372     return inAbstractNamespace(name, 0);
373   }
374 
375   /**
376    * Convenience method to create an {@link AFUNIXSocketAddress} in the abstract namespace.
377    *
378    * The returned socket address will use the byte representation of this identifier (using the
379    * system's default character encoding), prefixed with a null byte (to indicate the abstract
380    * namespace is used).
381    *
382    * @param name The identifier in the abstract namespace, without trailing zero or @.
383    * @param port The port associated with this socket, or {@code 0} when no port should be assigned.
384    * @return The address.
385    * @throws SocketException if the operation fails.
386    */
387   public static AFUNIXSocketAddress inAbstractNamespace(String name, int port)
388       throws SocketException {
389     byte[] bytes = name.getBytes(ADDRESS_CHARSET);
390     byte[] addr = new byte[bytes.length + 1];
391     System.arraycopy(bytes, 0, addr, 1, bytes.length);
392     return AFUNIXSocketAddress.of(addr, port);
393   }
394 
395   private static String prettyPrint(byte[] data) {
396     final int dataLength = data.length;
397     if (dataLength == 0) {
398       return "";
399     }
400     StringBuilder sb = new StringBuilder(dataLength + 16);
401     for (int i = 0; i < dataLength; i++) {
402       byte c = data[i];
403       if (c >= 32 && c < 127) {
404         sb.append((char) c);
405       } else {
406         sb.append("\\x");
407         sb.append(String.format(Locale.ENGLISH, "%02x", c));
408       }
409     }
410     return sb.toString();
411   }
412 
413   @Override
414   public String toString() {
415     int port = getPort();
416     return getClass().getName() + "[" + (port == 0 ? "" : "port=" + port + ";") + "path="
417         + prettyPrint(getBytes()) + "]";
418   }
419 
420   /**
421    * Returns the path to the UNIX domain socket, as a human-readable string using the default
422    * encoding.
423    *
424    * For addresses in the abstract namespace, the US_ASCII encoding is used; zero-bytes are
425    * converted to '@', other non-printable bytes are converted to '.'
426    *
427    * @return The path.
428    * @see #getPathAsBytes()
429    */
430   public String getPath() {
431     byte[] bytes = getBytes();
432     if (bytes.length == 0) {
433       return "";
434     } else if (bytes[0] != 0) {
435       return new String(bytes, ADDRESS_CHARSET);
436     }
437 
438     byte[] by = bytes.clone();
439     for (int i = 0; i < by.length; i++) {
440       byte b = by[i];
441       if (b == 0) {
442         by[i] = '@';
443       } else if (b >= 32 && b < 127) {
444         // print as-is
445       } else {
446         by[i] = '.';
447       }
448     }
449     return new String(by, StandardCharsets.US_ASCII);
450   }
451 
452   /**
453    * Returns the {@link Charset} used to encode/decode {@link AFUNIXSocketAddress}es.
454    *
455    * This is usually the system default charset, unless that is {@link StandardCharsets#US_ASCII}
456    * (7-bit), in which case {@link StandardCharsets#ISO_8859_1} is used instead.
457    *
458    * @return The charset.
459    */
460   public static Charset addressCharset() {
461     return ADDRESS_CHARSET;
462   }
463 
464   /**
465    * Returns the path to the UNIX domain socket, as bytes.
466    *
467    * @return The path.
468    * @see #getPath()
469    */
470   public byte[] getPathAsBytes() {
471     return getBytes().clone();
472   }
473 
474   /**
475    * Checks if the address is in the abstract namespace (or, for Haiku OS, in the internal
476    * namespace).
477    *
478    * @return {@code true} if the address is in the abstract namespace.
479    */
480   public boolean isInAbstractNamespace() {
481     byte[] bytes = getBytes();
482     return bytes.length > 0 && bytes[0] == 0;
483   }
484 
485   @Override
486   public boolean hasFilename() {
487     byte[] bytes = getBytes();
488     return bytes.length > 0 && bytes[0] != 0;
489   }
490 
491   @Override
492   public File getFile() throws FileNotFoundException {
493     if (isInAbstractNamespace()) {
494       throw new FileNotFoundException("Socket is in abstract namespace");
495     }
496     byte[] bytes = getBytes();
497 
498     if (bytes.length == 0) {
499       throw new FileNotFoundException("No name");
500     }
501     return new File(new String(bytes, ADDRESS_CHARSET));
502   }
503 
504   /**
505    * Checks if an {@link InetAddress} can be unwrapped to an {@link AFUNIXSocketAddress}.
506    *
507    * @param addr The instance to check.
508    * @return {@code true} if so.
509    * @see #wrapAddress()
510    * @see #unwrap(InetAddress, int)
511    */
512   public static boolean isSupportedAddress(InetAddress addr) {
513     return AFInetAddress.isSupportedAddress(addr, AF_UNIX);
514   }
515 
516   /**
517    * Checks if a {@link SocketAddress} can be unwrapped to an {@link AFUNIXSocketAddress}.
518    *
519    * @param addr The instance to check.
520    * @return {@code true} if so.
521    * @see #unwrap(InetAddress, int)
522    */
523   public static boolean isSupportedAddress(SocketAddress addr) {
524     return supportedAddressSupplier(addr) != null;
525   }
526 
527   /**
528    * Checks if the given address can be unwrapped to an {@link AFUNIXSocketAddress}, and if so,
529    * returns a supplier function; if not, {@code null} is returned.
530    *
531    * @param addr The address.
532    * @return The supplier, or {@code null}.
533    */
534   static AFSupplier<AFUNIXSocketAddress> supportedAddressSupplier(SocketAddress addr) {
535     if (addr == null) {
536       return null;
537     } else if (addr instanceof AFUNIXSocketAddress) {
538       return () -> ((AFUNIXSocketAddress) addr);
539     } else {
540       return SocketAddressUtil.supplyAFUNIXSocketAddress(addr);
541     }
542   }
543 
544   /**
545    * Returns the corresponding {@link AFAddressFamily}.
546    *
547    * @return The address family instance.
548    */
549   @SuppressWarnings("null")
550   public static AFAddressFamily<AFUNIXSocketAddress> addressFamily() {
551     return AFUNIXSelectorProvider.getInstance().addressFamily();
552   }
553 
554   @Override
555   public URI toURI(String scheme, URI template) throws IOException {
556     switch (scheme) {
557       case "unix":
558       case "file":
559         try {
560           if (getPort() > 0 && !"file".equals(scheme)) {
561             return new URI(scheme, null, "localhost", getPort(), getPath(), null, (String) null);
562           } else {
563             return new URI(scheme, null, null, -1, getPath(), null, null);
564           }
565         } catch (URISyntaxException e) {
566           throw new IOException(e);
567         }
568       case "http+unix":
569       case "https+unix":
570         HostAndPort hp = new HostAndPort(getPath(), getPort());
571         return hp.toURI(scheme, template);
572       default:
573         return super.toURI(scheme, template);
574     }
575   }
576 
577   @Override
578   public AFUNIXSocket newConnectedSocket() throws IOException {
579     return (AFUNIXSocket) super.newConnectedSocket();
580   }
581 
582   @Override
583   public AFUNIXServerSocket newBoundServerSocket() throws IOException {
584     return (AFUNIXServerSocket) super.newBoundServerSocket();
585   }
586 
587   @Override
588   public AFUNIXServerSocket newForceBoundServerSocket() throws IOException {
589     return (AFUNIXServerSocket) super.newForceBoundServerSocket();
590   }
591 }