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