1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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
48
49
50
51
52
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
77
78
79
80
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
90
91
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
130 }
131
132
133 int response = in.read();
134 if (response != 1) {
135 if (response == -1) {
136
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
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
188
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
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
228
229
230
231
232
233
234 @Override
235 @SuppressFBWarnings("EI_EXPOSE_REP")
236 public final FileDescriptor getFileDescriptor() {
237 return fd;
238 }
239
240
241
242
243
244
245
246
247
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
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
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 }