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 }