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