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