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.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   public final void writeExternal(ObjectOutput objOut) throws IOException {
107     if (fd == null || !fd.valid()) {
108       throw new IOException("No or invalid file descriptor");
109     }
110     final int randomValue = ThreadLocalRandom.current().nextInt();
111 
112     int localPort;
113     try {
114       AFServerSocket<?> serverSocket = (AFServerSocket<?>) socketFactory.createServerSocket(0);
115       localPort = serverSocket.getLocalPort();
116 
117       AFSocketServer<?> server = new AFSocketServer<AFSocketAddress>(serverSocket) {
118         @Override
119         protected void doServeSocket(AFSocket<?> socket) throws IOException {
120           AFUNIXSocket unixSocket = (AFUNIXSocket) socket;
121           try (DataOutputStream out = new DataOutputStream(socket.getOutputStream());
122               InputStream in = socket.getInputStream();) {
123             unixSocket.setOutboundFileDescriptors(fd);
124             out.writeInt(randomValue);
125 
126             try {
127               socket.setSoTimeout(CONNECT_TIMEOUT);
128             } catch (IOException e) {
129               // ignore
130             }
131 
132             // This call blocks until the remote is done with the file descriptor, or we time out.
133             int response = in.read();
134             if (response != 1) {
135               if (response == -1) {
136                 // EOF, remote terminated
137               } else {
138                 throw new IOException("Unexpected response: " + response);
139               }
140             }
141           } finally {
142             stop();
143           }
144         }
145 
146         @Override
147         protected void onServerStopped(AFServerSocket<?> socket) {
148           try {
149             serverSocket.close();
150           } catch (IOException e) {
151             // ignore
152           }
153         }
154 
155       };
156       @SuppressWarnings("unused")
157       ScheduledFuture<IOException> unused = server.startThenStopAfter(SERVER_TIMEOUT,
158           TimeUnit.MILLISECONDS);
159     } catch (IOException e) {
160       objOut.writeObject(e);
161       throw e;
162     }
163 
164     objOut.writeObject(socketFactory);
165     objOut.writeInt(magicValue);
166     objOut.writeInt(randomValue);
167     objOut.writeInt(localPort);
168     objOut.flush();
169   }
170 
171   @SuppressWarnings("resource")
172   @Override
173   public final void readExternal(ObjectInput objIn) throws IOException, ClassNotFoundException {
174     DataInputStream in1 = remoteConnection.getAndSet(null);
175     if (in1 != null) {
176       in1.close();
177     }
178 
179     Object obj = objIn.readObject();
180     if (obj instanceof IOException) {
181       IOException e = new IOException("Could not read RemoteFileDescriptor");
182       e.addSuppressed((IOException) obj);
183       throw e;
184     }
185     this.socketFactory = (AFUNIXRMISocketFactory) obj;
186 
187     // Since ancillary messages can only be read in combination with real data, we read and verify a
188     // magic value
189     this.magicValue = objIn.readInt();
190     if ((magicValue & MAGIC_VALUE_MASK) != MAGIC_VALUE_MASK) {
191       throw new IOException("Unexpected magic value: " + Integer.toHexString(magicValue));
192     }
193     final int randomValue = objIn.readInt();
194     int port = objIn.readInt();
195 
196     AFUNIXSocket socket = (AFUNIXSocket) socketFactory.createSocket("", port);
197     if (remoteServer.getAndSet(socket) != null) {
198       throw new IllegalStateException("remoteServer was not null");
199     }
200 
201     try {
202       socket.setSoTimeout(CONNECT_TIMEOUT);
203     } catch (IOException e) {
204       // ignore
205     }
206 
207     in1 = new DataInputStream(socket.getInputStream());
208     this.remoteConnection.set(in1);
209     socket.ensureAncillaryReceiveBufferSize(128);
210 
211     int random = in1.readInt();
212 
213     if (random != randomValue) {
214       throw new IOException("Invalid socket connection");
215     }
216     FileDescriptor[] descriptors = socket.getReceivedFileDescriptors();
217 
218     if (descriptors == null || descriptors.length != 1) {
219       throw new IOException("Did not receive exactly 1 file descriptor but " + (descriptors == null
220           ? 0 : descriptors.length));
221     }
222 
223     this.fd = descriptors[0];
224   }
225 
226   /**
227    * Returns the file descriptor.
228    *
229    * This is either the original one that was specified in the constructor or a copy that was sent
230    * via RMI over an AF_UNIX connection as part of an ancillary message.
231    *
232    * @return The file descriptor.
233    */
234   @Override
235   @SuppressFBWarnings("EI_EXPOSE_REP")
236   public final FileDescriptor getFileDescriptor() {
237     return fd;
238   }
239 
240   /**
241    * Returns the "magic value" for this type of file descriptor.
242    *
243    * The magic value consists of an indicator ("this is a file descriptor") as well as its
244    * capabilities (read/write). It is used to prevent, for example, converting an output stream to
245    * an input stream.
246    *
247    * @return The magic value.
248    */
249   protected final int getMagicValue() {
250     return magicValue;
251   }
252 
253   @SuppressWarnings("resource")
254   @Override
255   public void close() throws IOException {
256     DataInputStream in1 = remoteConnection.getAndSet(null);
257     if (in1 != null) {
258       try {
259         in1.close();
260       } catch (SocketException e) {
261         // ignore
262       }
263     }
264 
265     AFUNIXSocket remoteSocket = remoteServer.getAndSet(null);
266     if (remoteSocket != null) {
267       try (OutputStream out = remoteSocket.getOutputStream()) {
268         out.write(1);
269       } catch (SocketException e) {
270         // ignore
271       }
272       remoteSocket.close();
273     }
274 
275     @SuppressWarnings("null")
276     T c = this.resource.getAndSet(null);
277     if (c != null) {
278       if (c instanceof Closeable) {
279         ((Closeable) c).close();
280       }
281     }
282   }
283 
284   private static int parseTimeoutMillis(String s, boolean zeroPermitted) {
285     Objects.requireNonNull(s);
286     int duration;
287     try {
288       duration = Integer.parseInt(s);
289     } catch (Exception e) {
290       throw new IllegalArgumentException("Illegal timeout value: " + s, e);
291     }
292     if (duration < 0 || (duration == 0 && !zeroPermitted)) {
293       throw new IllegalArgumentException("Illegal timeout value: " + s);
294     }
295     return duration;
296   }
297 }