1 /*
2  * Copyright (c) 2017, 2018, Oracle and/or its affiliates. All rights reserved.
3  * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
4  *
5  * This code is free software; you can redistribute it and/or modify it
6  * under the terms of the GNU General Public License version 2 only, as
7  * published by the Free Software Foundation.
8  *
9  * This code is distributed in the hope that it will be useful, but WITHOUT
10  * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
11  * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
12  * version 2 for more details (a copy is included in the LICENSE file that
13  * accompanied this code).
14  *
15  * You should have received a copy of the GNU General Public License version
16  * 2 along with this work; if not, write to the Free Software Foundation,
17  * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
18  *
19  * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
20  * or visit www.oracle.com if you need additional information or have any
21  * questions.
22  */
23 
24 import java.io.ByteArrayOutputStream;
25 import java.io.FilterInputStream;
26 import java.io.IOException;
27 import java.io.InputStream;
28 import java.io.OutputStream;
29 import java.io.Serializable;
30 import java.net.InetAddress;
31 import java.net.ServerSocket;
32 import java.net.Socket;
33 import java.net.SocketAddress;
34 import java.net.SocketException;
35 import java.net.SocketOption;
36 import java.nio.channels.ServerSocketChannel;
37 import java.nio.channels.SocketChannel;
38 import java.rmi.server.RMIClientSocketFactory;
39 import java.rmi.server.RMIServerSocketFactory;
40 import java.rmi.server.RMISocketFactory;
41 import java.util.ArrayList;
42 import java.util.Arrays;
43 import java.util.List;
44 import java.util.Objects;
45 import java.util.Set;
46 
47 import org.testng.Assert;
48 import org.testng.annotations.Test;
49 import org.testng.annotations.DataProvider;
50 
51 /*
52  * @test
53  * @summary TestSocket Factory and tests of the basic trigger, match, and replace functions
54  * @run testng TestSocketFactory
55  * @bug 8186539
56  */
57 
58 /**
59  * A RMISocketFactory utility factory to log RMI stream contents and to
60  * trigger, and then match and replace output stream contents to simulate failures.
61  * <p>
62  * The trigger is a sequence of bytes that must be found before looking
63  * for the bytes to match and replace.  If the trigger sequence is empty
64  * matching is immediately enabled. While waiting for the trigger to be found
65  * bytes written to the streams are written through to the output stream.
66  * The when triggered and when a trigger is non-empty, matching looks for
67  * the sequence of bytes supplied.  If the sequence is empty, no matching or
68  * replacement is performed.
69  * While waiting for a complete match, the partial matched bytes are not
70  * written to the output stream.  When the match is incomplete, the partial
71  * matched bytes are written to the output.  When a match is complete the
72  * full replacement byte array is written to the output.
73  * <p>
74  * The trigger, match, and replacement bytes arrays can be changed at any
75  * time and immediately reset and restart matching.  Changes are propagated
76  * to all of the sockets created from the factories immediately.
77  */
78 public class TestSocketFactory extends RMISocketFactory
79         implements RMIClientSocketFactory, RMIServerSocketFactory, Serializable {
80 
81     private static final long serialVersionUID = 1L;
82 
83     private volatile transient byte[] triggerBytes;
84 
85     private volatile transient byte[] matchBytes;
86 
87     private volatile transient byte[] replaceBytes;
88 
89     private transient final List<InterposeSocket> sockets = new ArrayList<>();
90 
91     private transient final List<InterposeServerSocket> serverSockets = new ArrayList<>();
92 
93     static final byte[] EMPTY_BYTE_ARRAY = new byte[0];
94 
95     public static final boolean DEBUG = false;
96 
97     /**
98      * Debugging output can be synchronized with logging of RMI actions.
99      *
100      * @param format a printf format
101      * @param args   any args
102      */
DEBUG(String format, Object... args)103     private static void DEBUG(String format, Object... args) {
104         if (DEBUG) {
105             System.err.printf(format, args);
106         }
107     }
108 
109     /**
110      * Create a socket factory that creates InputStreams
111      * and OutputStreams that log.
112      */
TestSocketFactory()113     public TestSocketFactory() {
114         this.triggerBytes = EMPTY_BYTE_ARRAY;
115         this.matchBytes = EMPTY_BYTE_ARRAY;
116         this.replaceBytes = EMPTY_BYTE_ARRAY;
117     }
118 
119     /**
120      * Set the match and replacement bytes, with an empty trigger.
121      * The match and replacements are propagated to all existing sockets.
122      *
123      * @param matchBytes bytes to match
124      * @param replaceBytes bytes to replace the matched bytes
125      */
setMatchReplaceBytes(byte[] matchBytes, byte[] replaceBytes)126     public void setMatchReplaceBytes(byte[] matchBytes, byte[] replaceBytes) {
127         setMatchReplaceBytes(EMPTY_BYTE_ARRAY, matchBytes, replaceBytes);
128     }
129 
130     /**
131      * Set the trigger, match, and replacement bytes.
132      * The trigger, match, and replacements are propagated to all existing sockets.
133      *
134      * @param triggerBytes array of bytes to use as a trigger, may be zero length
135      * @param matchBytes bytes to match after the trigger has been seen
136      * @param replaceBytes bytes to replace the matched bytes
137      */
setMatchReplaceBytes(byte[] triggerBytes, byte[] matchBytes, byte[] replaceBytes)138     public synchronized void setMatchReplaceBytes(byte[] triggerBytes, byte[] matchBytes,
139                                      byte[] replaceBytes) {
140         this.triggerBytes = Objects.requireNonNull(triggerBytes, "triggerBytes");
141         this.matchBytes = Objects.requireNonNull(matchBytes, "matchBytes");
142         this.replaceBytes = Objects.requireNonNull(replaceBytes, "replaceBytes");
143         sockets.forEach( s -> s.setMatchReplaceBytes(triggerBytes, matchBytes,
144                 replaceBytes));
145         serverSockets.forEach( s -> s.setMatchReplaceBytes(triggerBytes, matchBytes,
146                 replaceBytes));
147     }
148 
149     @Override
createSocket(String host, int port)150     public synchronized Socket createSocket(String host, int port) throws IOException {
151         Socket socket = RMISocketFactory.getDefaultSocketFactory()
152                 .createSocket(host, port);
153         InterposeSocket s = new InterposeSocket(socket,
154                 triggerBytes, matchBytes, replaceBytes);
155         sockets.add(s);
156         return s;
157     }
158 
159     /**
160      * Return the current list of sockets.
161      * @return Return a snapshot of the current list of sockets
162      */
getSockets()163     public synchronized List<InterposeSocket> getSockets() {
164         List<InterposeSocket> snap = new ArrayList<>(sockets);
165         return snap;
166     }
167 
168     @Override
createServerSocket(int port)169     public synchronized ServerSocket createServerSocket(int port) throws IOException {
170 
171         ServerSocket serverSocket = RMISocketFactory.getDefaultSocketFactory()
172                 .createServerSocket(port);
173         InterposeServerSocket ss = new InterposeServerSocket(serverSocket,
174                 triggerBytes, matchBytes, replaceBytes);
175         serverSockets.add(ss);
176         return ss;
177     }
178 
179     /**
180      * Return the current list of server sockets.
181      * @return Return a snapshot of the current list of server sockets
182      */
getServerSockets()183     public synchronized List<InterposeServerSocket> getServerSockets() {
184         List<InterposeServerSocket> snap = new ArrayList<>(serverSockets);
185         return snap;
186     }
187 
188     /**
189      * An InterposeSocket wraps a socket that produces InputStreams
190      * and OutputStreams that log the traffic.
191      * The OutputStreams it produces watch for a trigger and then
192      * match an array of bytes and replace them.
193      * Useful for injecting protocol and content errors.
194      */
195     public static class InterposeSocket extends Socket {
196         private final Socket socket;
197         private InputStream in;
198         private MatchReplaceOutputStream out;
199         private volatile byte[] triggerBytes;
200         private volatile byte[] matchBytes;
201         private volatile byte[] replaceBytes;
202         private final ByteArrayOutputStream inLogStream;
203         private final ByteArrayOutputStream outLogStream;
204         private final String name;
205         private static volatile int num = 0;    // index for created Interpose509s
206 
207         /**
208          * Construct a socket that interposes on a socket to match and replace.
209          * The trigger is empty.
210          * @param socket the underlying socket
211          * @param matchBytes the bytes that must match
212          * @param replaceBytes the replacement bytes
213          */
InterposeSocket(Socket socket, byte[] matchBytes, byte[] replaceBytes)214         public InterposeSocket(Socket socket, byte[] matchBytes, byte[] replaceBytes) {
215             this(socket, EMPTY_BYTE_ARRAY, matchBytes, replaceBytes);
216         }
217 
218         /**
219          * Construct a socket that interposes on a socket to match and replace.
220          * @param socket the underlying socket
221          * @param triggerBytes array of bytes to enable matching
222          * @param matchBytes the bytes that must match
223          * @param replaceBytes the replacement bytes
224          */
InterposeSocket(Socket socket, byte[] triggerBytes, byte[] matchBytes, byte[] replaceBytes)225         public InterposeSocket(Socket socket, byte[]
226                 triggerBytes, byte[] matchBytes, byte[] replaceBytes) {
227             this.socket = socket;
228             this.triggerBytes = Objects.requireNonNull(triggerBytes, "triggerBytes");
229             this.matchBytes = Objects.requireNonNull(matchBytes, "matchBytes");
230             this.replaceBytes = Objects.requireNonNull(replaceBytes, "replaceBytes");
231             this.inLogStream = new ByteArrayOutputStream();
232             this.outLogStream = new ByteArrayOutputStream();
233             this.name = "IS" + ++num + "::"
234                     + Thread.currentThread().getName() + ": "
235                     + socket.getLocalPort() + " <  " + socket.getPort();
236         }
237 
238         /**
239          * Set the match and replacement bytes, with an empty trigger.
240          * The match and replacements are propagated to all existing sockets.
241          *
242          * @param matchBytes bytes to match
243          * @param replaceBytes bytes to replace the matched bytes
244          */
setMatchReplaceBytes(byte[] matchBytes, byte[] replaceBytes)245         public void setMatchReplaceBytes(byte[] matchBytes, byte[] replaceBytes) {
246             this.setMatchReplaceBytes(EMPTY_BYTE_ARRAY, matchBytes, replaceBytes);
247         }
248 
249         /**
250          * Set the trigger, match, and replacement bytes.
251          * The trigger, match, and replacements are propagated to the
252          * MatchReplaceOutputStream, if it has been created.
253          *
254          * @param triggerBytes array of bytes to use as a trigger, may be zero length
255          * @param matchBytes bytes to match after the trigger has been seen
256          * @param replaceBytes bytes to replace the matched bytes
257          */
setMatchReplaceBytes(byte[] triggerBytes, byte[] matchBytes, byte[] replaceBytes)258         public synchronized void setMatchReplaceBytes(byte[] triggerBytes, byte[] matchBytes,
259                                          byte[] replaceBytes) {
260             this.triggerBytes = triggerBytes;
261             this.matchBytes = matchBytes;
262             this.replaceBytes = replaceBytes;
263             if (out != null) {
264                 out.setMatchReplaceBytes(triggerBytes, matchBytes, replaceBytes);
265             } else {
266                 DEBUG("InterposeSocket.setMatchReplaceBytes with out == null%n");
267             }
268         }
269 
270         @Override
connect(SocketAddress endpoint)271         public void connect(SocketAddress endpoint) throws IOException {
272             socket.connect(endpoint);
273         }
274 
275         @Override
connect(SocketAddress endpoint, int timeout)276         public void connect(SocketAddress endpoint, int timeout) throws IOException {
277             socket.connect(endpoint, timeout);
278         }
279 
280         @Override
bind(SocketAddress bindpoint)281         public void bind(SocketAddress bindpoint) throws IOException {
282             socket.bind(bindpoint);
283         }
284 
285         @Override
getInetAddress()286         public InetAddress getInetAddress() {
287             return socket.getInetAddress();
288         }
289 
290         @Override
getLocalAddress()291         public InetAddress getLocalAddress() {
292             return socket.getLocalAddress();
293         }
294 
295         @Override
getPort()296         public int getPort() {
297             return socket.getPort();
298         }
299 
300         @Override
getLocalPort()301         public int getLocalPort() {
302             return socket.getLocalPort();
303         }
304 
305         @Override
getRemoteSocketAddress()306         public SocketAddress getRemoteSocketAddress() {
307             return socket.getRemoteSocketAddress();
308         }
309 
310         @Override
getLocalSocketAddress()311         public SocketAddress getLocalSocketAddress() {
312             return socket.getLocalSocketAddress();
313         }
314 
315         @Override
getChannel()316         public SocketChannel getChannel() {
317             return socket.getChannel();
318         }
319 
320         @Override
close()321         public synchronized void close() throws IOException {
322             socket.close();
323         }
324 
325         @Override
toString()326         public String toString() {
327             return "InterposeSocket " + name + ": " + socket.toString();
328         }
329 
330         @Override
isConnected()331         public boolean isConnected() {
332             return socket.isConnected();
333         }
334 
335         @Override
isBound()336         public boolean isBound() {
337             return socket.isBound();
338         }
339 
340         @Override
isClosed()341         public boolean isClosed() {
342             return socket.isClosed();
343         }
344 
345         @Override
setOption(SocketOption<T> name, T value)346         public <T> Socket setOption(SocketOption<T> name, T value) throws IOException {
347             return socket.setOption(name, value);
348         }
349 
350         @Override
getOption(SocketOption<T> name)351         public <T> T getOption(SocketOption<T> name) throws IOException {
352             return socket.getOption(name);
353         }
354 
355         @Override
supportedOptions()356         public Set<SocketOption<?>> supportedOptions() {
357             return socket.supportedOptions();
358         }
359 
360         @Override
getInputStream()361         public synchronized InputStream getInputStream() throws IOException {
362             if (in == null) {
363                 in = socket.getInputStream();
364                 String name = Thread.currentThread().getName() + ": "
365                         + socket.getLocalPort() + " <  " + socket.getPort();
366                 in = new LoggingInputStream(in, name, inLogStream);
367                 DEBUG("Created new LoggingInputStream: %s%n", name);
368             }
369             return in;
370         }
371 
372         @Override
getOutputStream()373         public synchronized OutputStream getOutputStream() throws IOException {
374             if (out == null) {
375                 OutputStream o = socket.getOutputStream();
376                 String name = Thread.currentThread().getName() + ": "
377                         + socket.getLocalPort() + "  > " + socket.getPort();
378                 out = new MatchReplaceOutputStream(o, name, outLogStream,
379                         triggerBytes, matchBytes, replaceBytes);
380                 DEBUG("Created new MatchReplaceOutputStream: %s%n", name);
381             }
382             return out;
383         }
384 
385         /**
386          * Return the bytes logged from the input stream.
387          * @return Return the bytes logged from the input stream.
388          */
getInLogBytes()389         public byte[] getInLogBytes() {
390             return inLogStream.toByteArray();
391         }
392 
393         /**
394          * Return the bytes logged from the output stream.
395          * @return Return the bytes logged from the output stream.
396          */
getOutLogBytes()397         public byte[] getOutLogBytes() {
398             return outLogStream.toByteArray();
399         }
400 
401     }
402 
403     /**
404      * InterposeServerSocket is a ServerSocket that wraps each Socket it accepts
405      * with an InterposeSocket so that its input and output streams can be monitored.
406      */
407     public static class InterposeServerSocket extends ServerSocket {
408         private final ServerSocket socket;
409         private volatile byte[] triggerBytes;
410         private volatile byte[] matchBytes;
411         private volatile byte[] replaceBytes;
412         private final List<InterposeSocket> sockets = new ArrayList<>();
413 
414         /**
415          * Construct a server socket that interposes on a socket to match and replace.
416          * The trigger is empty.
417          * @param socket the underlying socket
418          * @param matchBytes the bytes that must match
419          * @param replaceBytes the replacement bytes
420          */
InterposeServerSocket(ServerSocket socket, byte[] matchBytes, byte[] replaceBytes)421         public InterposeServerSocket(ServerSocket socket, byte[] matchBytes,
422                                      byte[] replaceBytes) throws IOException {
423             this(socket, EMPTY_BYTE_ARRAY, matchBytes, replaceBytes);
424         }
425 
426         /**
427          * Construct a server socket that interposes on a socket to match and replace.
428          * @param socket the underlying socket
429          * @param triggerBytes array of bytes to enable matching
430          * @param matchBytes the bytes that must match
431          * @param replaceBytes the replacement bytes
432          */
InterposeServerSocket(ServerSocket socket, byte[] triggerBytes, byte[] matchBytes, byte[] replaceBytes)433         public InterposeServerSocket(ServerSocket socket, byte[] triggerBytes,
434                                      byte[] matchBytes, byte[] replaceBytes) throws IOException {
435             this.socket = socket;
436             this.triggerBytes = Objects.requireNonNull(triggerBytes, "triggerBytes");
437             this.matchBytes = Objects.requireNonNull(matchBytes, "matchBytes");
438             this.replaceBytes = Objects.requireNonNull(replaceBytes, "replaceBytes");
439         }
440 
441         /**
442          * Set the match and replacement bytes, with an empty trigger.
443          * The match and replacements are propagated to all existing sockets.
444          *
445          * @param matchBytes bytes to match
446          * @param replaceBytes bytes to replace the matched bytes
447          */
setMatchReplaceBytes(byte[] matchBytes, byte[] replaceBytes)448         public void setMatchReplaceBytes(byte[] matchBytes, byte[] replaceBytes) {
449             setMatchReplaceBytes(EMPTY_BYTE_ARRAY, matchBytes, replaceBytes);
450         }
451 
452         /**
453          * Set the trigger, match, and replacement bytes.
454          * The trigger, match, and replacements are propagated to all existing sockets.
455          *
456          * @param triggerBytes array of bytes to use as a trigger, may be zero length
457          * @param matchBytes bytes to match after the trigger has been seen
458          * @param replaceBytes bytes to replace the matched bytes
459          */
setMatchReplaceBytes(byte[] triggerBytes, byte[] matchBytes, byte[] replaceBytes)460         public synchronized void setMatchReplaceBytes(byte[] triggerBytes, byte[] matchBytes,
461                                          byte[] replaceBytes) {
462             this.triggerBytes = triggerBytes;
463             this.matchBytes = matchBytes;
464             this.replaceBytes = replaceBytes;
465             sockets.forEach(s -> s.setMatchReplaceBytes(triggerBytes, matchBytes, replaceBytes));
466         }
467         /**
468          * Return a snapshot of the current list of sockets created from this server socket.
469          * @return Return a snapshot of the current list of sockets
470          */
getSockets()471         public synchronized List<InterposeSocket> getSockets() {
472             List<InterposeSocket> snap = new ArrayList<>(sockets);
473             return snap;
474         }
475 
476         @Override
bind(SocketAddress endpoint)477         public void bind(SocketAddress endpoint) throws IOException {
478             socket.bind(endpoint);
479         }
480 
481         @Override
bind(SocketAddress endpoint, int backlog)482         public void bind(SocketAddress endpoint, int backlog) throws IOException {
483             socket.bind(endpoint, backlog);
484         }
485 
486         @Override
getInetAddress()487         public InetAddress getInetAddress() {
488             return socket.getInetAddress();
489         }
490 
491         @Override
getLocalPort()492         public int getLocalPort() {
493             return socket.getLocalPort();
494         }
495 
496         @Override
getLocalSocketAddress()497         public SocketAddress getLocalSocketAddress() {
498             return socket.getLocalSocketAddress();
499         }
500 
501         @Override
accept()502         public Socket accept() throws IOException {
503             Socket s = socket.accept();
504             synchronized(this) {
505                 InterposeSocket aSocket = new InterposeSocket(s, matchBytes,
506                         replaceBytes);
507                 sockets.add(aSocket);
508                 return aSocket;
509             }
510         }
511 
512         @Override
close()513         public void close() throws IOException {
514             socket.close();
515         }
516 
517         @Override
getChannel()518         public ServerSocketChannel getChannel() {
519             return socket.getChannel();
520         }
521 
522         @Override
isClosed()523         public boolean isClosed() {
524             return socket.isClosed();
525         }
526 
527         @Override
toString()528         public String toString() {
529             return socket.toString();
530         }
531 
532         @Override
setOption(SocketOption<T> name, T value)533         public <T> ServerSocket setOption(SocketOption<T> name, T value)
534                 throws IOException {
535             return socket.setOption(name, value);
536         }
537 
538         @Override
getOption(SocketOption<T> name)539         public <T> T getOption(SocketOption<T> name) throws IOException {
540             return socket.getOption(name);
541         }
542 
543         @Override
supportedOptions()544         public Set<SocketOption<?>> supportedOptions() {
545             return socket.supportedOptions();
546         }
547 
548         @Override
setSoTimeout(int timeout)549         public synchronized void setSoTimeout(int timeout) throws SocketException {
550             socket.setSoTimeout(timeout);
551         }
552 
553         @Override
getSoTimeout()554         public synchronized int getSoTimeout() throws IOException {
555             return socket.getSoTimeout();
556         }
557     }
558 
559     /**
560      * LoggingInputStream is a stream and logs all bytes read to it.
561      * For identification it is given a name.
562      */
563     public static class LoggingInputStream extends FilterInputStream {
564         private int bytesIn = 0;
565         private final String name;
566         private final OutputStream log;
567 
LoggingInputStream(InputStream in, String name, OutputStream log)568         public LoggingInputStream(InputStream in, String name, OutputStream log) {
569             super(in);
570             this.name = name;
571             this.log = log;
572         }
573 
574         @Override
read()575         public int read() throws IOException {
576             int b = super.read();
577             if (b >= 0) {
578                 log.write(b);
579                 bytesIn++;
580             }
581             return b;
582         }
583 
584         @Override
read(byte[] b, int off, int len)585         public int read(byte[] b, int off, int len) throws IOException {
586             int bytes = super.read(b, off, len);
587             if (bytes > 0) {
588                 log.write(b, off, bytes);
589                 bytesIn += bytes;
590             }
591             return bytes;
592         }
593 
594         @Override
read(byte[] b)595         public int read(byte[] b) throws IOException {
596             return read(b, 0, b.length);
597         }
598 
599         @Override
close()600         public void close() throws IOException {
601             super.close();
602         }
603 
604         @Override
toString()605         public String toString() {
606             return String.format("%s: In: (%d)", name, bytesIn);
607         }
608     }
609 
610     /**
611      * An OutputStream that looks for a trigger to enable matching and
612      * replaces one string of bytes with another.
613      * If any range matches, the match starts after the partial match.
614      */
615     static class MatchReplaceOutputStream extends OutputStream {
616         private final OutputStream out;
617         private final String name;
618         private volatile byte[] triggerBytes;
619         private volatile byte[] matchBytes;
620         private volatile byte[] replaceBytes;
621         int triggerIndex;
622         int matchIndex;
623         private int bytesOut = 0;
624         private final OutputStream log;
625 
MatchReplaceOutputStream(OutputStream out, String name, OutputStream log, byte[] matchBytes, byte[] replaceBytes)626         MatchReplaceOutputStream(OutputStream out, String name, OutputStream log,
627                                  byte[] matchBytes, byte[] replaceBytes) {
628             this(out, name, log, EMPTY_BYTE_ARRAY, matchBytes, replaceBytes);
629         }
630 
MatchReplaceOutputStream(OutputStream out, String name, OutputStream log, byte[] triggerBytes, byte[] matchBytes, byte[] replaceBytes)631         MatchReplaceOutputStream(OutputStream out, String name, OutputStream log,
632                                  byte[] triggerBytes, byte[] matchBytes,
633                                  byte[] replaceBytes) {
634             this.out = out;
635             this.name = name;
636             this.triggerBytes = Objects.requireNonNull(triggerBytes, "triggerBytes");
637             triggerIndex = 0;
638             this.matchBytes = Objects.requireNonNull(matchBytes, "matchBytes");
639             this.replaceBytes = Objects.requireNonNull(replaceBytes, "replaceBytes");
640             matchIndex = 0;
641             this.log = log;
642         }
643 
setMatchReplaceBytes(byte[] matchBytes, byte[] replaceBytes)644         public void setMatchReplaceBytes(byte[] matchBytes, byte[] replaceBytes) {
645             setMatchReplaceBytes(EMPTY_BYTE_ARRAY, matchBytes, replaceBytes);
646         }
647 
setMatchReplaceBytes(byte[] triggerBytes, byte[] matchBytes, byte[] replaceBytes)648         public void setMatchReplaceBytes(byte[] triggerBytes, byte[] matchBytes,
649                                          byte[] replaceBytes) {
650             this.triggerBytes = Objects.requireNonNull(triggerBytes, "triggerBytes");
651             triggerIndex = 0;
652             this.matchBytes = Objects.requireNonNull(matchBytes, "matchBytes");
653             this.replaceBytes = Objects.requireNonNull(replaceBytes, "replaceBytes");
654             matchIndex = 0;
655         }
656 
657 
write(int b)658         public void write(int b) throws IOException {
659             b = b & 0xff;
660             if (matchBytes.length == 0) {
661                 // fast path, no match
662                 out.write(b);
663                 log.write(b);
664                 bytesOut++;
665                 return;
666             }
667             // if trigger not satisfied, keep looking
668             if (triggerBytes.length != 0 && triggerIndex < triggerBytes.length) {
669                 out.write(b);
670                 log.write(b);
671                 bytesOut++;
672 
673                 triggerIndex = (b == (triggerBytes[triggerIndex] & 0xff))
674                         ? ++triggerIndex    // matching advance
675                         : 0;                // no match, reset
676             } else {
677                 // trigger not used or has been satisfied
678                 if (b == (matchBytes[matchIndex] & 0xff)) {
679                     if (++matchIndex >= matchBytes.length) {
680                         matchIndex = 0;
681                         triggerIndex = 0;       // match/replace ok, reset trigger
682                         DEBUG("TestSocketFactory MatchReplace %s replaced %d bytes " +
683                                 "at offset: %d (x%04x)%n",
684                                 name, replaceBytes.length, bytesOut, bytesOut);
685                         out.write(replaceBytes);
686                         log.write(replaceBytes);
687                         bytesOut += replaceBytes.length;
688                     }
689                 } else {
690                     if (matchIndex > 0) {
691                         // mismatch, write out any that matched already
692                         DEBUG("Partial match %s matched %d bytes at offset: %d (0x%04x), " +
693                                 " expected: x%02x, actual: x%02x%n",
694                                 name, matchIndex, bytesOut, bytesOut, matchBytes[matchIndex], b);
695                         out.write(matchBytes, 0, matchIndex);
696                         log.write(matchBytes, 0, matchIndex);
697                         bytesOut += matchIndex;
698                         matchIndex = 0;
699                     }
700                     if (b == (matchBytes[matchIndex] & 0xff)) {
701                         matchIndex++;
702                     } else {
703                         out.write(b);
704                         log.write(b);
705                         bytesOut++;
706                     }
707                 }
708             }
709         }
710 
flush()711         public void flush() throws IOException {
712             if (matchIndex > 0) {
713                 // write out any that matched already to avoid consumer hang.
714                 // Match/replace across a flush is not supported.
715                 DEBUG( "Flush partial match %s matched %d bytes at offset: %d (0x%04x)%n",
716                         name, matchIndex, bytesOut, bytesOut);
717                 out.write(matchBytes, 0, matchIndex);
718                 log.write(matchBytes, 0, matchIndex);
719                 bytesOut += matchIndex;
720                 matchIndex = 0;
721             }
722         }
723 
724         @Override
toString()725         public String toString() {
726             return String.format("%s: Out: (%d)", name, bytesOut);
727         }
728     }
729 
730     private static byte[] obj1Data = new byte[] {
731             0x7e, 0x7e, 0x7e,
732             (byte) 0x80, 0x05,
733             0x7f, 0x7f, 0x7f,
734             0x73, 0x72, 0x00, 0x10, // TC_OBJECT, TC_CLASSDESC, length = 16
735             (byte)'j', (byte)'a', (byte)'v', (byte)'a', (byte)'.',
736             (byte)'l', (byte)'a', (byte)'n', (byte)'g', (byte)'.',
737             (byte)'n', (byte)'u', (byte)'m', (byte)'b', (byte)'e', (byte)'r'
738     };
739     private static byte[] obj1Result = new byte[] {
740             0x7e, 0x7e, 0x7e,
741             (byte) 0x80, 0x05,
742             0x7f, 0x7f, 0x7f,
743             0x73, 0x72, 0x00, 0x11, // TC_OBJECT, TC_CLASSDESC, length = 17
744             (byte)'j', (byte)'a', (byte)'v', (byte)'a', (byte)'.',
745             (byte)'l', (byte)'a', (byte)'n', (byte)'g', (byte)'.',
746             (byte)'I', (byte)'n', (byte)'t', (byte)'e', (byte)'g', (byte)'e', (byte)'r'
747     };
748     private static byte[] obj1Trigger = new byte[] {
749             (byte) 0x80, 0x05
750     };
751     private static byte[] obj1Trigger2 = new byte[] {
752             0x7D, 0x7D, 0x7D, 0x7D,
753     };
754     private static byte[] obj1Trigger3 = new byte[] {
755             0x7F,
756     };
757     private static byte[] obj1Match = new byte[] {
758             0x73, 0x72, 0x00, 0x10, // TC_OBJECT, TC_CLASSDESC, length = 16
759             (byte)'j', (byte)'a', (byte)'v', (byte)'a', (byte)'.',
760             (byte)'l', (byte)'a', (byte)'n', (byte)'g', (byte)'.',
761             (byte)'n', (byte)'u', (byte)'m', (byte)'b', (byte)'e', (byte)'r'
762     };
763     private static byte[] obj1Repl = new byte[] {
764             0x73, 0x72, 0x00, 0x11, // TC_OBJECT, TC_CLASSDESC, length = 17
765             (byte)'j', (byte)'a', (byte)'v', (byte)'a', (byte)'.',
766             (byte)'l', (byte)'a', (byte)'n', (byte)'g', (byte)'.',
767             (byte)'I', (byte)'n', (byte)'t', (byte)'e', (byte)'g', (byte)'e', (byte)'r'
768     };
769 
770     @DataProvider(name = "MatchReplaceData")
matchReplaceData()771     static Object[][] matchReplaceData() {
772         byte[] empty = new byte[0];
773         byte[] byte1 = new byte[]{1, 2, 3, 4, 5, 6};
774         byte[] bytes2 = new byte[]{1, 2, 4, 3, 5, 6};
775         byte[] bytes3 = new byte[]{6, 5, 4, 3, 2, 1};
776         byte[] bytes4 = new byte[]{1, 2, 0x10, 0x20, 0x30, 0x40, 5, 6};
777         byte[] bytes4a = new byte[]{1, 2, 0x10, 0x20, 0x30, 0x40, 5, 7};  // mostly matches bytes4
778         byte[] bytes5 = new byte[]{0x30, 0x40, 5, 6};
779         byte[] bytes6 = new byte[]{1, 2, 0x10, 0x20, 0x30};
780 
781         return new Object[][]{
782                 {EMPTY_BYTE_ARRAY, new byte[]{}, new byte[]{},
783                         empty, empty},
784                 {EMPTY_BYTE_ARRAY, new byte[]{}, new byte[]{},
785                         byte1, byte1},
786                 {EMPTY_BYTE_ARRAY, new byte[]{3, 4}, new byte[]{4, 3},
787                         byte1, bytes2}, //swap bytes
788                 {EMPTY_BYTE_ARRAY, new byte[]{3, 4}, new byte[]{0x10, 0x20, 0x30, 0x40},
789                         byte1, bytes4}, // insert
790                 {EMPTY_BYTE_ARRAY, new byte[]{1, 2, 0x10, 0x20}, new byte[]{},
791                         bytes4, bytes5}, // delete head
792                 {EMPTY_BYTE_ARRAY, new byte[]{0x40, 5, 6}, new byte[]{},
793                         bytes4, bytes6},   // delete tail
794                 {EMPTY_BYTE_ARRAY, new byte[]{0x40, 0x50}, new byte[]{0x60, 0x50},
795                         bytes4, bytes4}, // partial match, replace nothing
796                 {EMPTY_BYTE_ARRAY, bytes4a, bytes3,
797                         bytes4, bytes4}, // long partial match, not replaced
798                 {EMPTY_BYTE_ARRAY, obj1Match, obj1Repl,
799                         obj1Match, obj1Repl},
800                 {obj1Trigger, obj1Match, obj1Repl,
801                         obj1Data, obj1Result},
802                 {obj1Trigger3, obj1Match, obj1Repl,
803                         obj1Data, obj1Result}, // different trigger, replace
804                 {obj1Trigger2, obj1Match, obj1Repl,
805                         obj1Data, obj1Data},  // no trigger, no replace
806         };
807     }
808 
809     @Test(dataProvider = "MatchReplaceData")
test1(byte[] trigger, byte[] match, byte[] replace, byte[] input, byte[] expected)810     public static void test1(byte[] trigger, byte[] match, byte[] replace,
811                       byte[] input, byte[] expected) {
812         System.out.printf("trigger: %s, match: %s, replace: %s%n", Arrays.toString(trigger),
813                 Arrays.toString(match), Arrays.toString(replace));
814         try (ByteArrayOutputStream output = new ByteArrayOutputStream();
815         ByteArrayOutputStream log = new ByteArrayOutputStream();
816              OutputStream out = new MatchReplaceOutputStream(output, "test3",
817                      log, trigger, match, replace)) {
818             out.write(input);
819             byte[] actual = output.toByteArray();
820             long index = Arrays.mismatch(actual, expected);
821 
822             if (index >= 0) {
823                 System.out.printf("array mismatch, offset: %d%n", index);
824                 System.out.printf("actual: %s%n", Arrays.toString(actual));
825                 System.out.printf("expected: %s%n", Arrays.toString(expected));
826             }
827             Assert.assertEquals(actual, expected, "match/replace fail");
828         } catch (IOException ioe) {
829             Assert.fail("unexpected exception", ioe);
830         }
831     }
832 }
833