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.rmi;
19  
20  import java.io.Closeable;
21  import java.io.DataInputStream;
22  import java.io.DataOutputStream;
23  import java.io.Externalizable;
24  import java.io.FileDescriptor;
25  import java.io.IOException;
26  import java.io.InputStream;
27  import java.io.ObjectInput;
28  import java.io.ObjectOutput;
29  import java.io.OutputStream;
30  import java.net.SocketException;
31  import java.util.Objects;
32  import java.util.concurrent.ScheduledFuture;
33  import java.util.concurrent.ThreadLocalRandom;
34  import java.util.concurrent.TimeUnit;
35  import java.util.concurrent.atomic.AtomicReference;
36  
37  import org.newsclub.net.unix.AFServerSocket;
38  import org.newsclub.net.unix.AFSocket;
39  import org.newsclub.net.unix.AFSocketAddress;
40  import org.newsclub.net.unix.AFUNIXSocket;
41  import org.newsclub.net.unix.FileDescriptorAccess;
42  import org.newsclub.net.unix.server.AFSocketServer;
43  
44  import com.kohlschutter.annotations.compiletime.SuppressFBWarnings;
45  
46  /**
47   * A wrapper that allows a {@link FileDescriptor} be sent via RMI over AF_UNIX sockets.
48   *
49   * @author Christian Kohlschütter
50   * @param <T> The resource type.
51   * @see RemoteFileInput
52   * @see RemoteFileOutput
53   */
54  public abstract class RemoteFileDescriptorBase<T> implements Externalizable, Closeable,
55      FileDescriptorAccess {
56    private static final String PROP_SERVER_TIMEOUT =
57        "org.newsclub.net.unix.rmi.rfd-server-timeout-millis";
58    private static final String PROP_CONNECT_TIMEOUT =
59        "org.newsclub.net.unix.rmi.rfd-connect-timeout-millis";
60  
61    private static final int SERVER_TIMEOUT = //
62        parseTimeoutMillis(System.getProperty(PROP_SERVER_TIMEOUT, "10000"), false);
63    private static final int CONNECT_TIMEOUT = //
64        parseTimeoutMillis(System.getProperty(PROP_CONNECT_TIMEOUT, "1000"), true);
65  
66    static final int MAGIC_VALUE_MASK = 0x00FD0000;
67    static final int BIT_READABLE = 1 << 0;
68    static final int BIT_WRITABLE = 1 << 1;
69  
70    private static final long serialVersionUID = 1L;
71  
72    private final AtomicReference<DataInputStream> remoteConnection = new AtomicReference<>();
73    private final AtomicReference<AFUNIXSocket> remoteServer = new AtomicReference<>();
74  
75    /**
76     * An optional, closeable resource that is related to this instance. If the reference is non-null,
77     * this will be closed upon {@link #close()}.
78     *
79     * For unidirectional implementations, this could be the corresponding input/output stream. For
80     * bidirectional implementations (e.g., a Socket, Pipe, etc.), this should close both directions.
81     */
82    protected final transient AtomicReference<T> resource = new AtomicReference<>();
83  
84    private int magicValue;
85    private transient FileDescriptor fd;
86    private AFUNIXRMISocketFactory socketFactory;
87  
88    /**
89     * Creates an uninitialized instance; used for externalization.
90     *
91     * @see #readExternal(ObjectInput)
92     */
93    public RemoteFileDescriptorBase() {
94    }
95  
96    RemoteFileDescriptorBase(AFUNIXRMISocketFactory socketFactory, T stream, FileDescriptor fd,
97        int magicValue) {
98      this.resource.set(stream);
99      this.socketFactory = socketFactory;
100     this.fd = fd;
101     this.magicValue = magicValue;
102   }
103 
104   @Override
105   @SuppressWarnings("PMD.ExceptionAsFlowControl")
106   @SuppressFBWarnings("PREDICTABLE_RANDOM")
107   public final synchronized void writeExternal(ObjectOutput objOut) throws IOException {
108     if (fd == null || !fd.valid()) {
109       throw new IOException("No or invalid file descriptor");
110     }
111     final int randomValue = ThreadLocalRandom.current().nextInt();
112 
113     int localPort;
114     try {
115       AFServerSocket<?> serverSocket = (AFServerSocket<?>) socketFactory.createServerSocket(0);
116       localPort = serverSocket.getLocalPort();
117 
118       AFSocketServer<?> server = new AFSocketServer<AFSocketAddress>(serverSocket) {
119         @Override
120         protected void doServeSocket(AFSocket<?> socket) throws IOException {
121           AFUNIXSocket unixSocket = (AFUNIXSocket) socket;
122           try (DataOutputStream out = new DataOutputStream(socket.getOutputStream());
123               InputStream in = socket.getInputStream();) {
124             unixSocket.setOutboundFileDescriptors(fd);
125             out.writeInt(randomValue);
126 
127             try {
128               socket.setSoTimeout(CONNECT_TIMEOUT);
129             } catch (IOException e) {
130               // ignore
131             }
132 
133             // This call blocks until the remote is done with the file descriptor, or we time out.
134             int response = in.read();
135             if (response != 1) {
136               if (response == -1) {
137                 // EOF, remote terminated
138               } else {
139                 throw new IOException("Unexpected response: " + response);
140               }
141             }
142           } finally {
143             stop();
144           }
145         }
146 
147         @Override
148         protected void onServerStopped(AFServerSocket<?> socket) {
149           try {
150             serverSocket.close();
151           } catch (IOException e) {
152             // ignore
153           }
154         }
155 
156       };
157       @SuppressWarnings("unused")
158       ScheduledFuture<IOException> unused = server.startThenStopAfter(SERVER_TIMEOUT,
159           TimeUnit.MILLISECONDS);
160     } catch (IOException e) {
161       objOut.writeObject(e);
162       throw e;
163     }
164 
165     objOut.writeObject(socketFactory);
166     objOut.writeInt(magicValue);
167     objOut.writeInt(randomValue);
168     objOut.writeInt(localPort);
169     objOut.flush();
170   }
171 
172   @SuppressWarnings("resource")
173   @SuppressFBWarnings("OBJECT_DESERIALIZATION")
174   @Override
175   public final synchronized void readExternal(ObjectInput objIn) throws IOException,
176       ClassNotFoundException {
177     DataInputStream in1 = remoteConnection.getAndSet(null);
178     if (in1 != null) {
179       in1.close();
180     }
181 
182     Object obj = objIn.readObject();
183     if (obj instanceof IOException) {
184       IOException e = new IOException("Could not read RemoteFileDescriptor");
185       e.addSuppressed((IOException) obj);
186       throw e;
187     }
188     this.socketFactory = (AFUNIXRMISocketFactory) obj;
189 
190     // Since ancillary messages can only be read in combination with real data, we read and verify a
191     // magic value
192     this.magicValue = objIn.readInt();
193     if ((magicValue & MAGIC_VALUE_MASK) != MAGIC_VALUE_MASK) {
194       throw new IOException("Unexpected magic value: " + Integer.toHexString(magicValue));
195     }
196     final int randomValue = objIn.readInt();
197     int port = objIn.readInt();
198 
199     AFUNIXSocket socket = (AFUNIXSocket) socketFactory.createSocket("", port);
200     if (remoteServer.getAndSet(socket) != null) {
201       throw new IllegalStateException("remoteServer was not null");
202     }
203 
204     try {
205       socket.setSoTimeout(CONNECT_TIMEOUT);
206     } catch (IOException e) {
207       // ignore
208     }
209 
210     in1 = new DataInputStream(socket.getInputStream());
211     this.remoteConnection.set(in1);
212     socket.ensureAncillaryReceiveBufferSize(128);
213 
214     int random = in1.readInt();
215 
216     if (random != randomValue) {
217       throw new IOException("Invalid socket connection");
218     }
219     FileDescriptor[] descriptors = socket.getReceivedFileDescriptors();
220 
221     if (descriptors == null || descriptors.length != 1) {
222       throw new IOException("Did not receive exactly 1 file descriptor but " + (descriptors == null
223           ? 0 : descriptors.length));
224     }
225 
226     this.fd = descriptors[0];
227   }
228 
229   /**
230    * Returns the file descriptor.
231    *
232    * This is either the original one that was specified in the constructor or a copy that was sent
233    * via RMI over an AF_UNIX connection as part of an ancillary message.
234    *
235    * @return The file descriptor.
236    */
237   @Override
238   @SuppressFBWarnings("EI_EXPOSE_REP")
239   public final FileDescriptor getFileDescriptor() {
240     return fd;
241   }
242 
243   /**
244    * Returns the "magic value" for this type of file descriptor.
245    *
246    * The magic value consists of an indicator ("this is a file descriptor") as well as its
247    * capabilities (read/write). It is used to prevent, for example, converting an output stream to
248    * an input stream.
249    *
250    * @return The magic value.
251    */
252   protected final int getMagicValue() {
253     return magicValue;
254   }
255 
256   @SuppressWarnings("resource")
257   @Override
258   public void close() throws IOException {
259     DataInputStream in1 = remoteConnection.getAndSet(null);
260     if (in1 != null) {
261       try {
262         in1.close();
263       } catch (SocketException e) {
264         // ignore
265       }
266     }
267 
268     AFUNIXSocket remoteSocket = remoteServer.getAndSet(null);
269     if (remoteSocket != null) {
270       try (OutputStream out = remoteSocket.getOutputStream()) {
271         out.write(1);
272       } catch (SocketException e) {
273         // ignore
274       }
275       remoteSocket.close();
276     }
277 
278     @SuppressWarnings("null")
279     T c = this.resource.getAndSet(null);
280     if (c != null) {
281       if (c instanceof Closeable) {
282         ((Closeable) c).close();
283       }
284     }
285   }
286 
287   private static int parseTimeoutMillis(String s, boolean zeroPermitted) {
288     Objects.requireNonNull(s);
289     int duration;
290     try {
291       duration = Integer.parseInt(s);
292     } catch (Exception e) {
293       throw new IllegalArgumentException("Illegal timeout value: " + s, e);
294     }
295     if (duration < 0 || (duration == 0 && !zeroPermitted)) {
296       throw new IllegalArgumentException("Illegal timeout value: " + s);
297     }
298     return duration;
299   }
300 }