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