AFSocket.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.Closeable;
import java.io.FileDescriptor;
import java.io.IOException;
import java.net.Socket;
import java.net.SocketAddress;
import java.net.SocketException;
import java.net.SocketImpl;
import java.util.concurrent.atomic.AtomicBoolean;
import org.eclipse.jdt.annotation.NonNull;
import org.eclipse.jdt.annotation.Nullable;
import com.kohlschutter.annotations.compiletime.SuppressFBWarnings;
/**
* junixsocket's base implementation of a {@link Socket}.
*
* @param <A> The concrete {@link AFSocketAddress} that is supported by this type.
* @author Christian Kohlschütter
*/
@SuppressWarnings({"PMD.CouplingBetweenObjects", "PMD.CyclomaticComplexity"})
public abstract class AFSocket<A extends AFSocketAddress> extends Socket implements AFSomeSocket,
AFSocketExtensions {
static final String PROP_LIBRARY_DISABLE_CAPABILITY_PREFIX =
"org.newsclub.net.unix.library.disable.";
private static final byte[] ZERO_BYTES = new byte[0];
@SuppressWarnings("PMD.MutableStaticState")
static String loadedLibrary; // set by NativeLibraryLoader
private static Integer capabilitiesValue = null;
private final AFSocketImpl<A> impl;
private final AFSocketAddressFromHostname<A> afh;
private final Closeables closeables = new Closeables();
private final AtomicBoolean created = new AtomicBoolean(false);
@SuppressWarnings("this-escape")
private final AFSocketChannel<A> channel = newChannel();
private @Nullable SocketAddressFilter connectFilter;
/**
* Creates a new {@link AFSocket} instance.
*
* @param impl The corresponding {@link SocketImpl} class.
* @param afh The conversion helper to get a socket address from an encoded hostname.
* @throws SocketException on error.
*/
@SuppressFBWarnings("CT_CONSTRUCTOR_THROW")
protected AFSocket(final AFSocketImpl<A> impl, AFSocketAddressFromHostname<A> afh)
throws SocketException {
super(impl);
this.afh = afh;
this.impl = impl;
}
/**
* Returns the {@link AFSocketAddress} type supported by this socket.
*
* @return The supported {@link AFSocketAddress}.
*/
protected final Class<? extends AFSocketAddress> socketAddressClass() {
return getAFImpl(false).getAddressFamily().getSocketAddressClass();
}
/**
* Creates a new {@link AFSocketChannel} for this socket.
*
* @return The new instance.
*/
protected abstract AFSocketChannel<A> newChannel();
/**
* The reference to the constructor of an {@link AFSocket} subclass.
*
* @param <A> The concrete {@link AFSocketAddress} that is supported by this type.
*/
@FunctionalInterface
public interface Constructor<A extends AFSocketAddress> {
/**
* Constructs a new {@link AFSocket} subclass instance.
*
* @param fdObj The file descriptor.
* @param factory The socket factory instance.
* @return The instance.
* @throws SocketException on error.
*/
@NonNull
AFSocket<A> newInstance(FileDescriptor fdObj, AFSocketFactory<A> factory)
throws SocketException;
}
static <A extends AFSocketAddress> AFSocket<A> newInstance(Constructor<A> constr,
AFSocketFactory<A> sf, FileDescriptor fdObj, int localPort, int remotePort)
throws IOException {
if (!fdObj.valid()) {
throw new SocketException("Invalid file descriptor");
}
int status = NativeUnixSocket.socketStatus(fdObj);
if (status == NativeUnixSocket.SOCKETSTATUS_INVALID) {
throw new SocketException("Not a valid socket");
}
AFSocket<A> socket = newInstance0(constr, fdObj, sf);
socket.getAFImpl().updatePorts(localPort, remotePort);
switch (status) {
case NativeUnixSocket.SOCKETSTATUS_CONNECTED:
socket.internalDummyConnect();
break;
case NativeUnixSocket.SOCKETSTATUS_BOUND:
socket.internalDummyBind();
break;
case NativeUnixSocket.SOCKETSTATUS_UNKNOWN:
break;
default:
throw new IllegalStateException("Invalid socketStatus response: " + status);
}
socket.getAFImpl().setSocketAddress(socket.getLocalSocketAddress());
return socket;
}
/**
* Creates a new, unbound {@link AFSocket}.
*
* This "default" implementation is a bit "lenient" with respect to the specification.
*
* In particular, we may ignore calls to {@link Socket#getTcpNoDelay()} and
* {@link Socket#setTcpNoDelay(boolean)}.
*
* @param <A> The corresponding address type.
* @param constr The implementation's {@link AFSocket} constructor
* @param factory The corresponding socket factory, or {@code null}.
* @return A new, unbound socket.
* @throws SocketException if the operation fails.
*/
protected static final <A extends AFSocketAddress> AFSocket<A> newInstance(Constructor<A> constr,
AFSocketFactory<A> factory) throws SocketException {
return newInstance0(constr, null, factory);
}
private static <A extends AFSocketAddress> @NonNull AFSocket<A> newInstance0(
Constructor<A> constr, FileDescriptor fdObj, AFSocketFactory<A> factory)
throws SocketException {
return constr.newInstance(fdObj, factory);
}
/**
* Creates a new {@link AFSocket} and connects it to the given {@link AFSocketAddress}.
*
* @param <A> The corresponding address type.
* @param constr The implementation's {@link AFSocket} constructor
* @param addr The address to connect to.
* @return A new, connected socket.
* @throws IOException if the operation fails.
*/
protected static final <A extends AFSocketAddress> @NonNull AFSocket<A> connectTo(
Constructor<A> constr, A addr) throws IOException {
AFSocket<A> socket = constr.newInstance(null, null);
socket.connect(addr);
return socket;
}
/**
* Creates a new {@link AFSocket} and connects it to the given {@link AFSocketAddress} using the
* default implementation suited for that address type.
*
* @param <A> The corresponding address type.
* @param addr The address to connect to.
* @return A new, connected socket.
* @throws IOException if the operation fails.
*/
public static final <A extends AFSocketAddress> AFSocket<?> connectTo(@NonNull A addr)
throws IOException {
AFSocket<?> socket = addr.getAddressFamily().getSocketConstructor().newInstance(null, null);
socket.connect(addr);
return socket;
}
/**
* Not supported, since it's not necessary for client sockets.
*
* @see AFServerSocket
*/
@Override
public final void bind(SocketAddress bindpoint) throws IOException {
if (bindpoint == null) {
throw new IllegalArgumentException();
}
if (isClosed()) {
throw new SocketException("Socket is closed");
}
if (isBound()) {
throw new SocketException("Already bound");
}
preprocessSocketAddress(bindpoint);
throw new SocketException("Use AF*ServerSocket#bind or #bindOn");
}
@Override
public final boolean isBound() {
return impl.getFD().valid() && (super.isBound() || impl.isBound());
}
@Override
public final boolean isConnected() {
return impl.getFD().valid() && (super.isConnected() || impl.isConnected());
}
@Override
public final void connect(SocketAddress endpoint) throws IOException {
connect(endpoint, 0);
}
@Override
public final void connect(SocketAddress endpoint, int timeout) throws IOException {
connect0(endpoint, timeout);
}
private AFSocketAddress preprocessSocketAddress(SocketAddress endpoint) throws SocketException {
if (endpoint == null) {
throw new IllegalArgumentException("endpoint is null");
} else if (endpoint instanceof SentinelSocketAddress) {
return (AFSocketAddress) endpoint;
} else {
return AFSocketAddress.preprocessSocketAddress(socketAddressClass(), endpoint, afh);
}
}
final boolean connect0(SocketAddress endpoint, int timeout) throws IOException {
if (timeout < 0) {
throw new IllegalArgumentException("connect: timeout can't be negative");
}
if (isClosed()) {
throw new SocketException("Socket is closed");
}
if (connectFilter != null) {
endpoint = connectFilter.apply(endpoint);
}
AFSocketAddress address = preprocessSocketAddress(endpoint);
if (!isBound()) {
internalDummyBind();
}
boolean success = getAFImpl().connect0(address, timeout);
if (success) {
int port = address.getPort();
if (port > 0) {
getAFImpl().updatePorts(getLocalPort(), port);
}
}
internalDummyConnect();
return success;
}
final void internalDummyConnect() throws IOException {
if (!isConnected()) {
super.connect(AFSocketAddress.INTERNAL_DUMMY_CONNECT, 0);
}
}
final void internalDummyBind() throws IOException {
if (!isBound()) {
super.bind(AFSocketAddress.INTERNAL_DUMMY_BIND);
}
}
@Override
public final String toString() {
return getClass().getName() + "@" + Integer.toHexString(hashCode()) + toStringSuffix();
}
final String toStringSuffix() {
if (impl.getFD().valid()) {
return "[local=" + getLocalSocketAddress() + ";remote=" + getRemoteSocketAddress() + "]";
} else {
return "[invalid]";
}
}
/**
* Returns <code>true</code> iff {@link AFSocket}s are supported by the current Java VM.
*
* To support {@link AFSocket}s, a custom JNI library must be loaded that is supplied with
* <em>junixsocket</em>.
*
* @return {@code true} iff supported.
*/
public static boolean isSupported() {
return NativeUnixSocket.isLoaded();
}
/**
* Checks if {@link AFSocket}s are supported by the current Java VM.
*
* If not, an {@link UnsupportedOperationException} is thrown.
*
* @throws UnsupportedOperationException if not supported.
*/
public static void ensureSupported() throws UnsupportedOperationException {
NativeUnixSocket.ensureSupported();
}
/**
* Returns the version of the junixsocket library, as a string, for debugging purposes.
*
* NOTE: Do not rely on the format of the version identifier, use socket capabilities instead.
*
* @return String The version identifier, or {@code null} if it could not be determined.
* @see #supports(AFSocketCapability)
*/
public static final String getVersion() {
String v = BuildProperties.getBuildProperties().get("git.build.version");
if (v != null && !v.startsWith("$")) {
return v;
}
try {
return NativeLibraryLoader.getJunixsocketVersion();
} catch (IOException e) {
return null;
}
}
/**
* Returns an identifier of the loaded native library, or {@code null} if the library hasn't been
* loaded yet.
*
* The identifier is useful mainly for debugging purposes.
*
* @return The identifier of the loaded junixsocket-native library, or {@code null}.
*/
public static final String getLoadedLibrary() {
return loadedLibrary;
}
@Override
public final boolean isClosed() {
return super.isClosed() || (isConnected() && !impl.getFD().valid()) || impl.isClosed();
}
@Override
public final int getAncillaryReceiveBufferSize() {
return impl.getAncillaryReceiveBufferSize();
}
@Override
public final void setAncillaryReceiveBufferSize(int size) {
impl.setAncillaryReceiveBufferSize(size);
}
@Override
public final void ensureAncillaryReceiveBufferSize(int minSize) {
impl.ensureAncillaryReceiveBufferSize(minSize);
}
private static boolean isCapDisabled(AFSocketCapability cap) {
return Boolean.parseBoolean(System.getProperty(PROP_LIBRARY_DISABLE_CAPABILITY_PREFIX + cap
.name(), "false"));
}
private static int initCapabilities() {
if (!isSupported()) {
return 0;
} else {
int v = NativeUnixSocket.capabilities();
if (System.getProperty("osv.version") != null) {
// no fork, no redirect...
v &= ~(AFSocketCapability.CAPABILITY_FD_AS_REDIRECT.getBitmask());
}
for (AFSocketCapability cap : AFSocketCapability.values()) {
if (isCapDisabled(cap)) {
v &= ~(cap.getBitmask());
}
}
return v;
}
}
private static synchronized int capabilities() {
if (capabilitiesValue == null) {
capabilitiesValue = initCapabilities();
}
return capabilitiesValue;
}
/**
* Checks if the current environment (system platform, native library, etc.) supports a given
* junixsocket capability.
*
* Deprecated. Please use {@link #supports(AFSocketCapability)} instead.
*
* NOTE: The result may or may not be cached from a previous call or from a check upon
* initialization.
*
* @param capability The capability.
* @return true if supported.
* @see #supports(AFSocketCapability)
*/
@Deprecated
public static final boolean supports(AFUNIXSocketCapability capability) {
return (capabilities() & capability.getBitmask()) != 0;
}
/**
* Checks if the current environment (system platform, native library, etc.) supports a given
* junixsocket capability.
*
* NOTE: The result may or may not be cached from a previous call or from a check upon
* initialization.
*
* @param capability The capability.
* @return true if supported.
*/
public static final boolean supports(AFSocketCapability capability) {
return (capabilities() & capability.getBitmask()) != 0;
}
/**
* Checks if the current environment (system platform, native library, etc.) supports "unsafe"
* operations (as controlled via the {@link AFSocketCapability#CAPABILITY_UNSAFE} capability).
*
* If supported, the method returns normally. If not supported, an {@link IOException} is thrown.
*
* @throws IOException if "unsafe" operations are not supported.
* @see Unsafe
*/
public static final void ensureUnsafeSupported() throws IOException {
if (!AFSocket.supports(AFSocketCapability.CAPABILITY_UNSAFE)) {
throw new IOException("Unsafe operations are not supported in this environment");
}
}
@Override
public final synchronized void close() throws IOException {
IOException superException = null;
try {
super.close();
} catch (IOException e) {
superException = e;
}
closeables.close(superException);
}
/**
* Registers a {@link Closeable} that should be closed when this socket is closed.
*
* @param closeable The closeable.
*/
public final void addCloseable(Closeable closeable) {
closeables.add(closeable);
}
/**
* Unregisters a previously registered {@link Closeable}.
*
* @param closeable The closeable.
*/
public final void removeCloseable(Closeable closeable) {
closeables.remove(closeable);
}
final AFSocketImpl<A> getAFImpl() {
return getAFImpl(true);
}
final AFSocketImpl<A> getAFImpl(boolean createSocket) {
if (createSocket && created.compareAndSet(false, true)) {
try {
getSoTimeout(); // trigger create via java.net.Socket
} catch (SocketException e) {
// ignore
}
}
return impl;
}
@SuppressFBWarnings("EI_EXPOSE_REP")
@Override
public AFSocketChannel<A> getChannel() {
return channel;
}
@SuppressWarnings("null")
@Override
public final synchronized A getRemoteSocketAddress() {
if (!isConnected()) {
return null;
}
return impl.getRemoteSocketAddress();
}
@SuppressWarnings("null")
@Override
public final A getLocalSocketAddress() {
if (isClosed()) {
return null;
}
return impl.getLocalSocketAddress();
}
@Override
public final FileDescriptor getFileDescriptor() throws IOException {
return impl.getFileDescriptor();
}
@Override
public final AFInputStream getInputStream() throws IOException {
return getAFImpl().getInputStream();
}
@Override
public final AFOutputStream getOutputStream() throws IOException {
return getAFImpl().getOutputStream();
}
/**
* Returns the internal helper instance for address-specific extensions.
*
* @return The helper instance.
* @throws UnsupportedOperationException if such extensions are not supported for this address
* type.
*/
protected final AFSocketImplExtensions<A> getImplExtensions() {
return getAFImpl(false).getImplExtensions();
}
/**
* Forces the address to be used for any subsequent call to {@link #connect(SocketAddress)} to be
* the given one, regardless of what'll be passed there.
*
* @param endpoint The forced endpoint address.
* @return This instance.
*/
public final AFSocket<A> forceConnectAddress(SocketAddress endpoint) {
return connectHook((SocketAddress orig) -> {
return orig == null ? null : endpoint;
});
}
/**
* Sets the hook for any subsequent call to {@link #connect(SocketAddress)} or
* {@link #connect(SocketAddress, int)} to be the given function.
*
* The function can monitor events or even alter the target address.
*
* @param hook The function that gets called for each connect call.
* @return This instance.
*/
public final AFSocket<A> connectHook(SocketAddressFilter hook) {
this.connectFilter = hook;
return this;
}
/**
* Probes the status of the socket connection.
*
* This usually involves checking for {@link #isConnected()}, and if assumed connected, also
* sending a zero-length message to the remote.
*
* @return {@code true} if the connection is known to be closed, {@code false} if the connection
* is open/not closed or the condition is unknown.
* @throws IOException on an unexpected error.
*/
public boolean checkConnectionClosed() throws IOException {
if (!isConnected()) {
return true;
}
try {
if (!AFSocket.supports(AFSocketCapability.CAPABILITY_ZERO_LENGTH_SEND)) {
return false;
}
getOutputStream().write(ZERO_BYTES);
return false;
} catch (SocketClosedException e) {
return true;
} catch (IOException e) {
if (!isConnected()) {
return true;
} else {
throw e;
}
}
}
/**
* Checks if we're running on Android (as far as junixsocket is concerned).
*
* @return {@code true} if running on Android.
*/
public static boolean isRunningOnAndroid() {
return NativeLibraryLoader.isAndroid();
}
@Override
public void setShutdownOnClose(boolean enabled) {
getAFImpl().getCore().setShutdownOnClose(enabled);
}
}