AFInetAddress.java
/*
* junixsocket
*
* Copyright 2009-2024 Christian Kohlschütter
*
* 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
*
* 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.
*/
package org.newsclub.net.unix;
import java.io.UnsupportedEncodingException;
import java.net.DatagramPacket;
import java.net.Inet4Address;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.SocketAddress;
import java.net.SocketException;
import java.net.URLDecoder;
import java.net.URLEncoder;
import java.net.UnknownHostException;
import java.nio.charset.StandardCharsets;
import java.util.Locale;
import java.util.Objects;
/**
* A workaround to create an {@link InetAddress} for an {@link AFSocketAddress}.
*
* {@link DatagramPacket} internally requires InetAddress compatibility. Even if it pretends to
* accept {@link SocketAddress}es, it refuses anything other than {@link InetSocketAddress}
* <em>and</em> then even stores host and port separately.
*
* This implementation deserializes a specially crafted {@link InetAddress} with a hostname that
* encodes the raw bytes of an {@link AFSocketAddress}. We do this because the deserialization code
* path does not attempt DNS resolution (which would fail one way or another).
*
* The hostnames we use end with ".junixsocket", to distinguish them from regular hostnames.
*
* @author Christian Kohlschütter
*/
class AFInetAddress {
private static final byte[] LOCAL_AF = {0x7f, 0, 0, (byte) 0xaf};
private static final char PREFIX = '[';
private static final String MARKER_HEX_ENCODING = "%%";
static final String INETADDR_SUFFIX = ".junixsocket";
/**
* Encodes a junixsocket socketAddress into a string that is (somewhat) guaranteed to not be
* resolved by java.net code.
*
* Implementation detail: The "[" prefix (with the corresponding "]" suffix missing from the
* input) should cause an early {@link UnknownHostException} be thrown, which is caught within
* {@link InetSocketAddress#InetSocketAddress(String, int)}, causing the hostname be marked as
* "unresolved" (without an address set).
*
* @param socketAddress The socket address.
* @return A string, to be used when calling
* {@link InetSocketAddress#InetSocketAddress(String, int)}, etc.
*/
static final String createUnresolvedHostname(byte[] socketAddress, AFAddressFamily<?> af) {
StringBuilder sb = new StringBuilder(1 + socketAddress.length + INETADDR_SUFFIX.length() + 8);
sb.append(PREFIX);
try {
sb.append(URLEncoder.encode(new String(socketAddress, StandardCharsets.ISO_8859_1),
StandardCharsets.ISO_8859_1.toString()));
} catch (UnsupportedEncodingException e) {
throw new IllegalStateException(e);
}
sb.append('.');
sb.append(af.getJuxString());
sb.append(INETADDR_SUFFIX);
String str = sb.toString();
if (str.length() < 64 || str.getBytes(StandardCharsets.UTF_8).length <= 255) {
return str;
}
sb.setLength(0);
sb.append(PREFIX);
sb.append(MARKER_HEX_ENCODING);
for (int i = 0, n = socketAddress.length; i < n; i++) {
sb.append(String.format(Locale.ENGLISH, "%02x", socketAddress[i]));
}
sb.append('.');
sb.append(af.getJuxString());
sb.append(INETADDR_SUFFIX);
return sb.toString();
}
/**
* Creates an InetAddress that is considered "resolved" internally (using a static loopback
* address), without actually having to resolve the address via DNS, thus still carrying the
* "hostname" field containing a hostname as returned by
* {@link #createUnresolvedHostname(byte[],AFAddressFamily)}.
*
* @param socketAddress The socket address.
* @return The {@link InetAddress}.
*/
static final InetAddress wrapAddress(byte[] socketAddress, AFAddressFamily<?> af) {
Objects.requireNonNull(af);
if (socketAddress == null || socketAddress.length == 0) {
return null;
}
String hostname = createUnresolvedHostname(socketAddress, af);
byte[] bytes = hostname.getBytes(StandardCharsets.UTF_8);
if (bytes.length > 255) {
throw new IllegalStateException("junixsocket address is too long to wrap as InetAddress");
}
try {
return InetAddress.getByAddress(hostname, LOCAL_AF);
} catch (UnknownHostException e) {
throw new IllegalStateException(e);
}
}
static final byte[] unwrapAddress(InetAddress addr, AFAddressFamily<?> af)
throws SocketException {
Objects.requireNonNull(addr);
if (!isSupportedAddress(addr, af)) {
throw new SocketException("Unsupported address");
}
String hostname = addr.getHostName();
try {
return unwrapAddress(hostname, af);
} catch (IllegalArgumentException e) {
throw (SocketException) new SocketException("Unsupported address").initCause(e);
}
}
static final byte[] unwrapAddress(String hostname, AFAddressFamily<?> af) throws SocketException {
Objects.requireNonNull(hostname);
if (!hostname.endsWith(INETADDR_SUFFIX)) {
throw new SocketException("Unsupported address");
}
final int end = hostname.length() - INETADDR_SUFFIX.length();
char c;
int domDot = -1;
for (int i = end - 1; i >= 0; i--) {
c = hostname.charAt(i);
if (c == '.') {
domDot = i;
break;
}
}
String juxString = hostname.substring(domDot + 1, end);
if (AFAddressFamily.getAddressFamily(juxString) != af) { // NOPMD
throw new SocketException("Incompatible address");
}
String encodedHostname = hostname.substring(1, domDot);
if (encodedHostname.startsWith(MARKER_HEX_ENCODING)) {
// Hex-only encoding
int len = encodedHostname.length();
if ((len & 1) == 1) {
throw new IllegalStateException("Length of hex-encoded wrapping must be even");
}
byte[] unwrapped = new byte[(len - 2) / 2];
for (int i = 2, n = encodedHostname.length(), o = 0; i < n; i += 2, o++) {
int v = Integer.parseInt(encodedHostname.substring(i, i + 2), 16);
unwrapped[o] = (byte) (v & 0xFF);
}
return unwrapped;
} else {
// URL-encoding
try {
return URLDecoder.decode(encodedHostname, StandardCharsets.ISO_8859_1.toString()).getBytes(
StandardCharsets.ISO_8859_1);
} catch (UnsupportedEncodingException e) {
throw new IllegalStateException(e);
}
}
}
static boolean isSupportedAddress(InetAddress addr, AFAddressFamily<?> af) {
if (addr instanceof Inet4Address && addr.isLoopbackAddress()) {
String hostname = addr.getHostName();
return hostname.endsWith(af.getJuxInetAddressSuffix());
}
return false;
}
}