001 /* 002 * Licensed to the Apache Software Foundation (ASF) under one or more 003 * contributor license agreements. See the NOTICE file distributed with 004 * this work for additional information regarding copyright ownership. 005 * The ASF licenses this file to You under the Apache License, Version 2.0 006 * (the "License"); you may not use this file except in compliance with 007 * the License. You may obtain a copy of the License at 008 * 009 * http://www.apache.org/licenses/LICENSE-2.0 010 * 011 * Unless required by applicable law or agreed to in writing, software 012 * distributed under the License is distributed on an "AS IS" BASIS, 013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 014 * See the License for the specific language governing permissions and 015 * limitations under the License. 016 */ 017 018 package org.apache.commons.net.tftp; 019 020 import java.io.BufferedInputStream; 021 import java.io.BufferedOutputStream; 022 import java.io.File; 023 import java.io.FileInputStream; 024 import java.io.FileNotFoundException; 025 import java.io.FileOutputStream; 026 import java.io.IOException; 027 import java.io.InputStream; 028 import java.io.OutputStream; 029 import java.io.PrintStream; 030 import java.net.SocketTimeoutException; 031 import java.util.HashSet; 032 import java.util.Iterator; 033 034 import org.apache.commons.net.io.FromNetASCIIOutputStream; 035 import org.apache.commons.net.io.ToNetASCIIInputStream; 036 037 /** 038 * A fully multi-threaded tftp server. Can handle multiple clients at the same time. Implements RFC 039 * 1350 and wrapping block numbers for large file support. 040 * 041 * To launch, just create an instance of the class. An IOException will be thrown if the server 042 * fails to start for reasons such as port in use, port denied, etc. 043 * 044 * To stop, use the shutdown method. 045 * 046 * To check to see if the server is still running (or if it stopped because of an error), call the 047 * isRunning() method. 048 * 049 * By default, events are not logged to stdout/stderr. This can be changed with the 050 * setLog and setLogError methods. 051 * 052 * <p> 053 * Example usage is below: 054 * 055 * <code> 056 * public static void main(String[] args) throws Exception 057 * { 058 * if (args.length != 1) 059 * { 060 * System.out 061 * .println("You must provide 1 argument - the base path for the server to serve from."); 062 * System.exit(1); 063 * } 064 * 065 * TFTPServer ts = new TFTPServer(new File(args[0]), new File(args[0]), GET_AND_PUT); 066 * ts.setSocketTimeout(2000); 067 * 068 * System.out.println("TFTP Server running. Press enter to stop."); 069 * new InputStreamReader(System.in).read(); 070 * 071 * ts.shutdown(); 072 * System.out.println("Server shut down."); 073 * System.exit(0); 074 * } 075 * 076 * </code> 077 * 078 * 079 * @author <A HREF="mailto:daniel.armbrust.list@gmail.com">Dan Armbrust</A> 080 * @since 2.0 081 */ 082 083 public class TFTPServer implements Runnable 084 { 085 private static final int DEFAULT_TFTP_PORT = 69; 086 public static enum ServerMode { GET_ONLY, PUT_ONLY, GET_AND_PUT; } 087 088 private HashSet<TFTPTransfer> transfers_ = new HashSet<TFTPTransfer>(); 089 private volatile boolean shutdownServer = false; 090 private TFTP serverTftp_; 091 private File serverReadDirectory_; 092 private File serverWriteDirectory_; 093 private int port_; 094 private Exception serverException = null; 095 private ServerMode mode_; 096 097 /* /dev/null output stream (default) */ 098 private static final PrintStream nullStream = new PrintStream( 099 new OutputStream() { 100 @Override 101 public void write(int b){} 102 @Override 103 public void write(byte[] b) throws IOException {} 104 } 105 ); 106 107 // don't have access to a logger api, so we will log to these streams, which 108 // by default are set to a no-op logger 109 private PrintStream log_; 110 private PrintStream logError_; 111 112 private int maxTimeoutRetries_ = 3; 113 private int socketTimeout_; 114 private Thread serverThread; 115 116 117 /** 118 * Start a TFTP Server on the default port (69). Gets and Puts occur in the specified 119 * directories. 120 * 121 * The server will start in another thread, allowing this constructor to return immediately. 122 * 123 * If a get or a put comes in with a relative path that tries to get outside of the 124 * serverDirectory, then the get or put will be denied. 125 * 126 * GET_ONLY mode only allows gets, PUT_ONLY mode only allows puts, and GET_AND_PUT allows both. 127 * Modes are defined as int constants in this class. 128 * 129 * @param serverReadDirectory directory for GET requests 130 * @param serverWriteDirectory directory for PUT requests 131 * @param mode A value as specified above. 132 * @throws IOException if the server directory is invalid or does not exist. 133 */ 134 public TFTPServer(File serverReadDirectory, File serverWriteDirectory, ServerMode mode) 135 throws IOException 136 { 137 this(serverReadDirectory, serverWriteDirectory, DEFAULT_TFTP_PORT, mode, null, null); 138 } 139 140 /** 141 * Start a TFTP Server on the specified port. Gets and Puts occur in the specified directory. 142 * 143 * The server will start in another thread, allowing this constructor to return immediately. 144 * 145 * If a get or a put comes in with a relative path that tries to get outside of the 146 * serverDirectory, then the get or put will be denied. 147 * 148 * GET_ONLY mode only allows gets, PUT_ONLY mode only allows puts, and GET_AND_PUT allows both. 149 * Modes are defined as int constants in this class. 150 * 151 * @param serverReadDirectory directory for GET requests 152 * @param serverWriteDirectory directory for PUT requests 153 * @param mode A value as specified above. 154 * @param log Stream to write log message to. If not provided, uses System.out 155 * @param errorLog Stream to write error messages to. If not provided, uses System.err. 156 * @throws IOException if the server directory is invalid or does not exist. 157 */ 158 public TFTPServer(File serverReadDirectory, File serverWriteDirectory, int port, ServerMode mode, 159 PrintStream log, PrintStream errorLog) throws IOException 160 { 161 port_ = port; 162 mode_ = mode; 163 log_ = (log == null ? nullStream: log); 164 logError_ = (errorLog == null ? nullStream : errorLog); 165 launch(serverReadDirectory, serverWriteDirectory); 166 } 167 168 /** 169 * Set the max number of retries in response to a timeout. Default 3. Min 0. 170 * 171 * @param retries 172 */ 173 public void setMaxTimeoutRetries(int retries) 174 { 175 if (retries < 0) 176 { 177 throw new RuntimeException("Invalid Value"); 178 } 179 maxTimeoutRetries_ = retries; 180 } 181 182 /** 183 * Get the current value for maxTimeoutRetries 184 */ 185 public int getMaxTimeoutRetries() 186 { 187 return maxTimeoutRetries_; 188 } 189 190 /** 191 * Set the socket timeout in milliseconds used in transfers. Defaults to the value here: 192 * http://commons.apache.org/net/apidocs/org/apache/commons/net/tftp/TFTP.html#DEFAULT_TIMEOUT 193 * (5000 at the time I write this) Min value of 10. 194 */ 195 public void setSocketTimeout(int timeout) 196 { 197 if (timeout < 10) 198 { 199 throw new RuntimeException("Invalid Value"); 200 } 201 socketTimeout_ = timeout; 202 } 203 204 /** 205 * The current socket timeout used during transfers in milliseconds. 206 */ 207 public int getSocketTimeout() 208 { 209 return socketTimeout_; 210 } 211 212 /* 213 * start the server, throw an error if it can't start. 214 */ 215 private void launch(File serverReadDirectory, File serverWriteDirectory) throws IOException 216 { 217 log_.println("Starting TFTP Server on port " + port_ + ". Read directory: " 218 + serverReadDirectory + " Write directory: " + serverWriteDirectory 219 + " Server Mode is " + mode_); 220 221 serverReadDirectory_ = serverReadDirectory.getCanonicalFile(); 222 if (!serverReadDirectory_.exists() || !serverReadDirectory.isDirectory()) 223 { 224 throw new IOException("The server read directory " + serverReadDirectory_ 225 + " does not exist"); 226 } 227 228 serverWriteDirectory_ = serverWriteDirectory.getCanonicalFile(); 229 if (!serverWriteDirectory_.exists() || !serverWriteDirectory.isDirectory()) 230 { 231 throw new IOException("The server write directory " + serverWriteDirectory_ 232 + " does not exist"); 233 } 234 235 serverTftp_ = new TFTP(); 236 237 // This is the value used in response to each client. 238 socketTimeout_ = serverTftp_.getDefaultTimeout(); 239 240 // we want the server thread to listen forever. 241 serverTftp_.setDefaultTimeout(0); 242 243 serverTftp_.open(port_); 244 245 serverThread = new Thread(this); 246 serverThread.setDaemon(true); 247 serverThread.start(); 248 } 249 250 @Override 251 protected void finalize() throws Throwable 252 { 253 shutdown(); 254 } 255 256 /** 257 * check if the server thread is still running. 258 * 259 * @return true if running, false if stopped. 260 * @throws Exception throws the exception that stopped the server if the server is stopped from 261 * an exception. 262 */ 263 public boolean isRunning() throws Exception 264 { 265 if (shutdownServer && serverException != null) 266 { 267 throw serverException; 268 } 269 return !shutdownServer; 270 } 271 272 public void run() 273 { 274 try 275 { 276 while (!shutdownServer) 277 { 278 TFTPPacket tftpPacket; 279 280 tftpPacket = serverTftp_.receive(); 281 282 TFTPTransfer tt = new TFTPTransfer(tftpPacket); 283 synchronized(transfers_) 284 { 285 transfers_.add(tt); 286 } 287 288 Thread thread = new Thread(tt); 289 thread.setDaemon(true); 290 thread.start(); 291 } 292 } 293 catch (Exception e) 294 { 295 if (!shutdownServer) 296 { 297 serverException = e; 298 logError_.println("Unexpected Error in TFTP Server - Server shut down! + " + e); 299 } 300 } 301 finally 302 { 303 shutdownServer = true; // set this to true, so the launching thread can check to see if it started. 304 if (serverTftp_ != null && serverTftp_.isOpen()) 305 { 306 serverTftp_.close(); 307 } 308 } 309 } 310 311 /** 312 * Stop the tftp server (and any currently running transfers) and release all opened network 313 * resources. 314 */ 315 public void shutdown() 316 { 317 shutdownServer = true; 318 319 synchronized(transfers_) 320 { 321 Iterator<TFTPTransfer> it = transfers_.iterator(); 322 while (it.hasNext()) 323 { 324 it.next().shutdown(); 325 } 326 } 327 328 try 329 { 330 serverTftp_.close(); 331 } 332 catch (RuntimeException e) 333 { 334 // noop 335 } 336 337 try { 338 serverThread.join(); 339 } catch (InterruptedException e) { 340 // we've done the best we could, return 341 } 342 } 343 344 /* 345 * An instance of an ongoing transfer. 346 */ 347 private class TFTPTransfer implements Runnable 348 { 349 private TFTPPacket tftpPacket_; 350 351 private boolean shutdownTransfer = false; 352 353 TFTP transferTftp_ = null; 354 355 public TFTPTransfer(TFTPPacket tftpPacket) 356 { 357 tftpPacket_ = tftpPacket; 358 } 359 360 public void shutdown() 361 { 362 shutdownTransfer = true; 363 try 364 { 365 transferTftp_.close(); 366 } 367 catch (RuntimeException e) 368 { 369 // noop 370 } 371 } 372 373 public void run() 374 { 375 try 376 { 377 transferTftp_ = new TFTP(); 378 379 transferTftp_.beginBufferedOps(); 380 transferTftp_.setDefaultTimeout(socketTimeout_); 381 382 transferTftp_.open(); 383 384 if (tftpPacket_ instanceof TFTPReadRequestPacket) 385 { 386 handleRead(((TFTPReadRequestPacket) tftpPacket_)); 387 } 388 else if (tftpPacket_ instanceof TFTPWriteRequestPacket) 389 { 390 handleWrite((TFTPWriteRequestPacket) tftpPacket_); 391 } 392 else 393 { 394 log_.println("Unsupported TFTP request (" + tftpPacket_ + ") - ignored."); 395 } 396 } 397 catch (Exception e) 398 { 399 if (!shutdownTransfer) 400 { 401 logError_ 402 .println("Unexpected Error in during TFTP file transfer. Transfer aborted. " 403 + e); 404 } 405 } 406 finally 407 { 408 try 409 { 410 if (transferTftp_ != null && transferTftp_.isOpen()) 411 { 412 transferTftp_.endBufferedOps(); 413 transferTftp_.close(); 414 } 415 } 416 catch (Exception e) 417 { 418 // noop 419 } 420 synchronized(transfers_) 421 { 422 transfers_.remove(this); 423 } 424 } 425 } 426 427 /* 428 * Handle a tftp read request. 429 */ 430 private void handleRead(TFTPReadRequestPacket trrp) throws IOException, TFTPPacketException 431 { 432 InputStream is = null; 433 try 434 { 435 if (mode_ == ServerMode.PUT_ONLY) 436 { 437 transferTftp_.bufferedSend(new TFTPErrorPacket(trrp.getAddress(), trrp 438 .getPort(), TFTPErrorPacket.ILLEGAL_OPERATION, 439 "Read not allowed by server.")); 440 return; 441 } 442 443 try 444 { 445 is = new BufferedInputStream(new FileInputStream(buildSafeFile( 446 serverReadDirectory_, trrp.getFilename(), false))); 447 } 448 catch (FileNotFoundException e) 449 { 450 transferTftp_.bufferedSend(new TFTPErrorPacket(trrp.getAddress(), trrp 451 .getPort(), TFTPErrorPacket.FILE_NOT_FOUND, e.getMessage())); 452 return; 453 } 454 catch (Exception e) 455 { 456 transferTftp_.bufferedSend(new TFTPErrorPacket(trrp.getAddress(), trrp 457 .getPort(), TFTPErrorPacket.UNDEFINED, e.getMessage())); 458 return; 459 } 460 461 if (trrp.getMode() == TFTP.NETASCII_MODE) 462 { 463 is = new ToNetASCIIInputStream(is); 464 } 465 466 byte[] temp = new byte[TFTPDataPacket.MAX_DATA_LENGTH]; 467 468 TFTPPacket answer; 469 470 int block = 1; 471 boolean sendNext = true; 472 473 int readLength = TFTPDataPacket.MAX_DATA_LENGTH; 474 475 TFTPDataPacket lastSentData = null; 476 477 // We are reading a file, so when we read less than the 478 // requested bytes, we know that we are at the end of the file. 479 while (readLength == TFTPDataPacket.MAX_DATA_LENGTH && !shutdownTransfer) 480 { 481 if (sendNext) 482 { 483 readLength = is.read(temp); 484 if (readLength == -1) 485 { 486 readLength = 0; 487 } 488 489 lastSentData = new TFTPDataPacket(trrp.getAddress(), trrp.getPort(), block, 490 temp, 0, readLength); 491 transferTftp_.bufferedSend(lastSentData); 492 } 493 494 answer = null; 495 496 int timeoutCount = 0; 497 498 while (!shutdownTransfer 499 && (answer == null || !answer.getAddress().equals(trrp.getAddress()) || answer 500 .getPort() != trrp.getPort())) 501 { 502 // listen for an answer. 503 if (answer != null) 504 { 505 // The answer that we got didn't come from the 506 // expected source, fire back an error, and continue 507 // listening. 508 log_.println("TFTP Server ignoring message from unexpected source."); 509 transferTftp_.bufferedSend(new TFTPErrorPacket(answer.getAddress(), 510 answer.getPort(), TFTPErrorPacket.UNKNOWN_TID, 511 "Unexpected Host or Port")); 512 } 513 try 514 { 515 answer = transferTftp_.bufferedReceive(); 516 } 517 catch (SocketTimeoutException e) 518 { 519 if (timeoutCount >= maxTimeoutRetries_) 520 { 521 throw e; 522 } 523 // didn't get an ack for this data. need to resend 524 // it. 525 timeoutCount++; 526 transferTftp_.bufferedSend(lastSentData); 527 continue; 528 } 529 } 530 531 if (answer == null || !(answer instanceof TFTPAckPacket)) 532 { 533 if (!shutdownTransfer) 534 { 535 logError_ 536 .println("Unexpected response from tftp client during transfer (" 537 + answer + "). Transfer aborted."); 538 } 539 break; 540 } 541 else 542 { 543 // once we get here, we know we have an answer packet 544 // from the correct host. 545 TFTPAckPacket ack = (TFTPAckPacket) answer; 546 if (ack.getBlockNumber() != block) 547 { 548 /* 549 * The origional tftp spec would have called on us to resend the 550 * previous data here, however, that causes the SAS Syndrome. 551 * http://www.faqs.org/rfcs/rfc1123.html section 4.2.3.1 The modified 552 * spec says that we ignore a duplicate ack. If the packet was really 553 * lost, we will time out on receive, and resend the previous data at 554 * that point. 555 */ 556 sendNext = false; 557 } 558 else 559 { 560 // send the next block 561 block++; 562 if (block > 65535) 563 { 564 // wrap the block number 565 block = 0; 566 } 567 sendNext = true; 568 } 569 } 570 } 571 } 572 finally 573 { 574 try 575 { 576 if (is != null) 577 { 578 is.close(); 579 } 580 } 581 catch (IOException e) 582 { 583 // noop 584 } 585 } 586 } 587 588 /* 589 * handle a tftp write request. 590 */ 591 private void handleWrite(TFTPWriteRequestPacket twrp) throws IOException, 592 TFTPPacketException 593 { 594 OutputStream bos = null; 595 try 596 { 597 if (mode_ == ServerMode.GET_ONLY) 598 { 599 transferTftp_.bufferedSend(new TFTPErrorPacket(twrp.getAddress(), twrp 600 .getPort(), TFTPErrorPacket.ILLEGAL_OPERATION, 601 "Write not allowed by server.")); 602 return; 603 } 604 605 int lastBlock = 0; 606 String fileName = twrp.getFilename(); 607 608 try 609 { 610 File temp = buildSafeFile(serverWriteDirectory_, fileName, true); 611 if (temp.exists()) 612 { 613 transferTftp_.bufferedSend(new TFTPErrorPacket(twrp.getAddress(), twrp 614 .getPort(), TFTPErrorPacket.FILE_EXISTS, "File already exists")); 615 return; 616 } 617 bos = new BufferedOutputStream(new FileOutputStream(temp)); 618 619 if (twrp.getMode() == TFTP.NETASCII_MODE) 620 { 621 bos = new FromNetASCIIOutputStream(bos); 622 } 623 } 624 catch (Exception e) 625 { 626 transferTftp_.bufferedSend(new TFTPErrorPacket(twrp.getAddress(), twrp 627 .getPort(), TFTPErrorPacket.UNDEFINED, e.getMessage())); 628 return; 629 } 630 631 TFTPAckPacket lastSentAck = new TFTPAckPacket(twrp.getAddress(), twrp.getPort(), 0); 632 transferTftp_.bufferedSend(lastSentAck); 633 634 while (true) 635 { 636 // get the response - ensure it is from the right place. 637 TFTPPacket dataPacket = null; 638 639 int timeoutCount = 0; 640 641 while (!shutdownTransfer 642 && (dataPacket == null 643 || !dataPacket.getAddress().equals(twrp.getAddress()) || dataPacket 644 .getPort() != twrp.getPort())) 645 { 646 // listen for an answer. 647 if (dataPacket != null) 648 { 649 // The data that we got didn't come from the 650 // expected source, fire back an error, and continue 651 // listening. 652 log_.println("TFTP Server ignoring message from unexpected source."); 653 transferTftp_.bufferedSend(new TFTPErrorPacket(dataPacket.getAddress(), 654 dataPacket.getPort(), TFTPErrorPacket.UNKNOWN_TID, 655 "Unexpected Host or Port")); 656 } 657 658 try 659 { 660 dataPacket = transferTftp_.bufferedReceive(); 661 } 662 catch (SocketTimeoutException e) 663 { 664 if (timeoutCount >= maxTimeoutRetries_) 665 { 666 throw e; 667 } 668 // It didn't get our ack. Resend it. 669 transferTftp_.bufferedSend(lastSentAck); 670 timeoutCount++; 671 continue; 672 } 673 } 674 675 if (dataPacket != null && dataPacket instanceof TFTPWriteRequestPacket) 676 { 677 // it must have missed our initial ack. Send another. 678 lastSentAck = new TFTPAckPacket(twrp.getAddress(), twrp.getPort(), 0); 679 transferTftp_.bufferedSend(lastSentAck); 680 } 681 else if (dataPacket == null || !(dataPacket instanceof TFTPDataPacket)) 682 { 683 if (!shutdownTransfer) 684 { 685 logError_ 686 .println("Unexpected response from tftp client during transfer (" 687 + dataPacket + "). Transfer aborted."); 688 } 689 break; 690 } 691 else 692 { 693 int block = ((TFTPDataPacket) dataPacket).getBlockNumber(); 694 byte[] data = ((TFTPDataPacket) dataPacket).getData(); 695 int dataLength = ((TFTPDataPacket) dataPacket).getDataLength(); 696 int dataOffset = ((TFTPDataPacket) dataPacket).getDataOffset(); 697 698 if (block > lastBlock || (lastBlock == 65535 && block == 0)) 699 { 700 // it might resend a data block if it missed our ack 701 // - don't rewrite the block. 702 bos.write(data, dataOffset, dataLength); 703 lastBlock = block; 704 } 705 706 lastSentAck = new TFTPAckPacket(twrp.getAddress(), twrp.getPort(), block); 707 transferTftp_.bufferedSend(lastSentAck); 708 if (dataLength < TFTPDataPacket.MAX_DATA_LENGTH) 709 { 710 // end of stream signal - The tranfer is complete. 711 bos.close(); 712 713 // But my ack may be lost - so listen to see if I 714 // need to resend the ack. 715 for (int i = 0; i < maxTimeoutRetries_; i++) 716 { 717 try 718 { 719 dataPacket = transferTftp_.bufferedReceive(); 720 } 721 catch (SocketTimeoutException e) 722 { 723 // this is the expected route - the client 724 // shouldn't be sending any more packets. 725 break; 726 } 727 728 if (dataPacket != null 729 && (!dataPacket.getAddress().equals(twrp.getAddress()) || dataPacket 730 .getPort() != twrp.getPort())) 731 { 732 // make sure it was from the right client... 733 transferTftp_ 734 .bufferedSend(new TFTPErrorPacket(dataPacket 735 .getAddress(), dataPacket.getPort(), 736 TFTPErrorPacket.UNKNOWN_TID, 737 "Unexpected Host or Port")); 738 } 739 else 740 { 741 // This means they sent us the last 742 // datapacket again, must have missed our 743 // ack. resend it. 744 transferTftp_.bufferedSend(lastSentAck); 745 } 746 } 747 748 // all done. 749 break; 750 } 751 } 752 } 753 } 754 finally 755 { 756 if (bos != null) 757 { 758 bos.close(); 759 } 760 } 761 } 762 763 /* 764 * Utility method to make sure that paths provided by tftp clients do not get outside of the 765 * serverRoot directory. 766 */ 767 private File buildSafeFile(File serverDirectory, String fileName, boolean createSubDirs) 768 throws IOException 769 { 770 File temp = new File(serverDirectory, fileName); 771 temp = temp.getCanonicalFile(); 772 773 if (!isSubdirectoryOf(serverDirectory, temp)) 774 { 775 throw new IOException("Cannot access files outside of tftp server root."); 776 } 777 778 // ensure directory exists (if requested) 779 if (createSubDirs) 780 { 781 createDirectory(temp.getParentFile()); 782 } 783 784 return temp; 785 } 786 787 /* 788 * recursively create subdirectories 789 */ 790 private void createDirectory(File file) throws IOException 791 { 792 File parent = file.getParentFile(); 793 if (parent == null) 794 { 795 throw new IOException("Unexpected error creating requested directory"); 796 } 797 if (!parent.exists()) 798 { 799 // recurse... 800 createDirectory(parent); 801 } 802 803 if (parent.isDirectory()) 804 { 805 if (file.isDirectory()) 806 { 807 return; 808 } 809 boolean result = file.mkdir(); 810 if (!result) 811 { 812 throw new IOException("Couldn't create requested directory"); 813 } 814 } 815 else 816 { 817 throw new IOException( 818 "Invalid directory path - file in the way of requested folder"); 819 } 820 } 821 822 /* 823 * recursively check to see if one directory is a parent of another. 824 */ 825 private boolean isSubdirectoryOf(File parent, File child) 826 { 827 File childsParent = child.getParentFile(); 828 if (childsParent == null) 829 { 830 return false; 831 } 832 if (childsParent.equals(parent)) 833 { 834 return true; 835 } 836 else 837 { 838 return isSubdirectoryOf(parent, childsParent); 839 } 840 } 841 } 842 843 /** 844 * Set the stream object to log debug / informational messages. By default, this is a no-op 845 * 846 * @param log 847 */ 848 public void setLog(PrintStream log) 849 { 850 this.log_ = log; 851 } 852 853 /** 854 * Set the stream object to log error messsages. By default, this is a no-op 855 * 856 * @param logError 857 */ 858 public void setLogError(PrintStream logError) 859 { 860 this.logError_ = logError; 861 } 862 }