001/* 002 * Copyright (c) 2004-2010 The Regents of the University of California. 003 * All rights reserved. 004 * 005 * '$Author: crawl $' 006 * '$Date: 2015-08-24 22:48:48 +0000 (Mon, 24 Aug 2015) $' 007 * '$Revision: 33634 $' 008 * 009 * Permission is hereby granted, without written agreement and without 010 * license or royalty fees, to use, copy, modify, and distribute this 011 * software and its documentation for any purpose, provided that the above 012 * copyright notice and the following two paragraphs appear in all copies 013 * of this software. 014 * 015 * IN NO EVENT SHALL THE UNIVERSITY OF CALIFORNIA BE LIABLE TO ANY PARTY 016 * FOR DIRECT, INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES 017 * ARISING OUT OF THE USE OF THIS SOFTWARE AND ITS DOCUMENTATION, EVEN IF 018 * THE UNIVERSITY OF CALIFORNIA HAS BEEN ADVISED OF THE POSSIBILITY OF 019 * SUCH DAMAGE. 020 * 021 * THE UNIVERSITY OF CALIFORNIA SPECIFICALLY DISCLAIMS ANY WARRANTIES, 022 * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF 023 * MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE SOFTWARE 024 * PROVIDED HEREUNDER IS ON AN "AS IS" BASIS, AND THE UNIVERSITY OF 025 * CALIFORNIA HAS NO OBLIGATION TO PROVIDE MAINTENANCE, SUPPORT, UPDATES, 026 * ENHANCEMENTS, OR MODIFICATIONS. 027 * 028 */ 029 030package org.geon; 031 032import java.io.File; 033import java.io.FileInputStream; 034import java.io.FileOutputStream; 035import java.io.IOException; 036import java.io.InputStream; 037import java.io.OutputStream; 038import java.util.HashSet; 039import java.util.Hashtable; 040 041import javax.swing.JOptionPane; 042import javax.swing.JPasswordField; 043import javax.swing.JTextField; 044 045import com.jcraft.jsch.ChannelExec; 046import com.jcraft.jsch.JSch; 047import com.jcraft.jsch.Session; 048import com.jcraft.jsch.UserInfo; 049 050import ptolemy.actor.TypedAtomicActor; 051import ptolemy.actor.TypedIOPort; 052import ptolemy.actor.parameters.FilePortParameter; 053import ptolemy.actor.parameters.PortParameter; 054import ptolemy.data.IntToken; 055import ptolemy.data.StringToken; 056import ptolemy.data.expr.StringParameter; 057import ptolemy.data.type.BaseType; 058import ptolemy.kernel.CompositeEntity; 059import ptolemy.kernel.util.IllegalActionException; 060import ptolemy.kernel.util.NameDuplicationException; 061import ptolemy.kernel.util.Workspace; 062 063////////////////////////////////////////////////////////////////////////// 064//// Scp 065/** 066 * Connects to a remote host using Ssh2 protocol. 067 * 068 * Error conditions this actor must respond robustly to: 069 * 070 * Wrong identity file given. Host unreachable. Login unsuccessful. Session dies 071 * prematurely. 072 * 073 * This actor will keep the session open until it receives a different username 074 * and host combination. 075 * 076 * @author Efrat Jaeger 077 * @version $Id: Scp.java 33634 2015-08-24 22:48:48Z crawl $ 078 * @category.name remote 079 * @category.name connection 080 * @category.name file transfer 081 */ 082 083/** 084 * 085 * 086 * FIXME 087 * THIS ACTOR SHARES DUPLICATE CODE WITH Ssh2Exec. BEFORE MAKING CHANGES HERE 088 * FACTOR OUT THE DUPLICATED CODE FROM BOTH CLASSES. 089 * 090 * 091 */ 092 093public class Scp extends TypedAtomicActor { 094 095 /** 096 * Construct an SCP actor with the given container and name. Create the 097 * parameters, initialize their values. 098 * 099 * @param container 100 * The container. 101 * @param name 102 * The name of this actor. 103 * @exception IllegalActionException 104 * If the entity cannot be contained by the proposed 105 * container. 106 * @exception NameDuplicationException 107 * If the container already has an actor with this name. 108 */ 109 public Scp(CompositeEntity container, String name) 110 throws NameDuplicationException, IllegalActionException { 111 super(container, name); 112 113 // initialize our variables 114 _jsch = new JSch(); 115 _setIdentities = new HashSet<String>(); 116 117 // create all the ports 118 host = new PortParameter(this, "host"); 119 host.setStringMode(true); 120 user = new PortParameter(this, "user"); 121 user.setStringMode(true); 122 direction = new StringParameter(this, "direction"); 123 direction.setDisplayName("scp to/from remote"); 124 direction.addChoice("TO"); 125 direction.addChoice("FROM"); 126 direction.setExpression("TO"); 127 localFilePath = new FilePortParameter(this, "localFilePath"); 128 remoteFilePath = new FilePortParameter(this, "remoteFilePath"); 129 identity = new FilePortParameter(this, "identity"); 130 stdout = new TypedIOPort(this, "stdout", false, true); 131 stderr = new TypedIOPort(this, "stderr", false, true); 132 returncode = new TypedIOPort(this, "returncode", false, true); 133 errors = new TypedIOPort(this, "errors", false, true); 134 135 // Set the type constraints. 136 user.setTypeEquals(BaseType.STRING); 137 host.setTypeEquals(BaseType.STRING); 138 identity.setTypeEquals(BaseType.STRING); 139 stdout.setTypeEquals(BaseType.STRING); 140 stderr.setTypeEquals(BaseType.STRING); 141 returncode.setTypeEquals(BaseType.INT); 142 errors.setTypeEquals(BaseType.STRING); 143 144 _attachText("_iconDescription", "<svg>\n" + "<rect x=\"0\" y=\"0\" " 145 + "width=\"65\" height=\"50\" style=\"fill:gray\"/>\n" 146 + "<text x=\"5\" y=\"30\"" 147 + "style=\"font-size:25; fill:yellow; font-family:SansSerif\">" 148 + "SCP</text>\n" + "</svg>\n"); 149 } 150 151 // //////////////// Public ports and parameters /////////////////////// 152 153 /** 154 * Username on the SSH host to be connected to. 155 */ 156 public PortParameter user; 157 /** 158 * Host to connect to. 159 */ 160 public PortParameter host; 161 /** 162 * scp direction, from/to. 163 */ 164 public StringParameter direction; 165 /** 166 * Local file path. 167 */ 168 public FilePortParameter localFilePath; 169 /** 170 * Remote file path. 171 */ 172 public FilePortParameter remoteFilePath; 173 /** 174 * The file path for <i>userName</i>'s ssh identity file if the user wants 175 * to connect without having to enter the password all the time. 176 * 177 * <P> 178 * The user can browse this file as it is a parameter. 179 */ 180 public FilePortParameter identity; 181 /** 182 * The string representation of the file path for <i>userName</i>'s ssh 183 * identity file if the user wants to connect without having to enter the 184 * password all the time. 185 * 186 * <P> 187 * This is the input option for the identity file. 188 */ 189 public TypedIOPort stdout; 190 /** 191 * The error that were reported by the remote execution or while connecting. 192 */ 193 public TypedIOPort stderr; 194 /** 195 * The return code of the execution. 196 * 197 * <P> 198 * This port will return <i>0 (zero)</i> if the execution is not succesfull, 199 * and a positive integer if it is successful. 200 */ 201 public TypedIOPort returncode; 202 /** 203 * The string representation of all the errors that happened during the 204 * execution of the actor, if there are any. 205 */ 206 public TypedIOPort errors; 207 208 // ///////////////////////////////////////////////////////////////// 209 // // public methods //// 210 211 @Override 212 public Object clone(Workspace workspace) throws CloneNotSupportedException { 213 Scp newObject = (Scp) super.clone(workspace); 214 newObject._ackError = null; 215 newObject._jsch = new JSch(); 216 newObject._session = null; 217 newObject._setIdentities = new HashSet<String>(); 218 return newObject; 219 } 220 /** 221 * Send the token in the <i>value</i> parameter to the output. 222 * 223 * @exception IllegalActionException 224 * If it is thrown by the send() method sending out the 225 * token. 226 */ 227 @Override 228 public void fire() throws IllegalActionException { 229 super.fire(); 230 // by default we will try to connect with private/public keys. 231 boolean isPassAuth = false; 232 233 user.update(); 234 host.update(); 235 String strUser = ((StringToken) user.getToken()).stringValue(); 236 String strHost = ((StringToken) host.getToken()).stringValue(); 237 238 localFilePath.update(); 239 remoteFilePath.update(); 240 String lfile = ((StringToken) localFilePath.getToken()).stringValue(); 241 String rfile = ((StringToken) remoteFilePath.getToken()).stringValue(); 242 243 // Hack the path because we can't deal with "file:" or "file://" 244 if (lfile.startsWith("file:")) { 245 lfile = lfile.substring(5); 246 247 if (lfile.startsWith("//")) { 248 lfile = lfile.substring(2); 249 } 250 } 251 252 if (rfile.startsWith("file:")) { 253 rfile = rfile.substring(5); 254 255 if (rfile.startsWith("//")) { 256 rfile = rfile.substring(2); 257 } 258 } 259 260 identity.update(); 261 String strIdentity; 262 strIdentity = ((StringToken) identity.getToken()).stringValue(); 263 264 if (strIdentity != null && strIdentity.length() > 0) { 265 // Hack the path because we can't deal with "file:" or "file://" 266 if (strIdentity.startsWith("file:")) { 267 strIdentity = strIdentity.substring(5); 268 269 if (strIdentity.startsWith("//")) { 270 strIdentity = strIdentity.substring(2); 271 } 272 } 273 } else { 274 // We are now need to connect with password 275 isPassAuth = true; 276 } 277 278 try { 279 if (isPassAuth) { 280 // Use password authentication, where use will be prompted 281 // for password. The password should be valid for the session 282 283 _connect(strUser, strHost); 284 } else { 285 _connect(strUser, strHost, strIdentity); 286 } 287 288 String direct = direction.getExpression(); 289 if (direct.equals("TO")) { 290 scpTo(lfile, rfile); 291 } else if (direct.equals("FROM")) { 292 scpFrom(lfile, rfile); 293 } else 294 throw new IllegalActionException(this, "invalid command"); 295 296 } catch (IllegalActionException e) { 297 // caught an exception, so output it to the errors port 298 stdout.send(0, new StringToken("")); 299 stderr.send(0, new StringToken("")); 300 returncode.send(0, new IntToken(0)); 301 errors.send(0, new StringToken(e.getMessage())); 302 } 303 } 304 305 /** 306 * Terminate any sessions. This method is invoked exactly once per execution 307 * of an application. None of the other action methods should be be invoked 308 * after it. 309 * 310 * @exception IllegalActionException 311 * Not thrown in this base class. 312 */ 313 @Override 314 public void wrapup() throws IllegalActionException { 315 _disconnect(); 316 _ackError = ""; 317 } 318 319 // //////////////// Private Methods /////////////////////// 320 321 /** 322 * @throws IllegalActionException 323 * If the connection fails. 324 * 325 * FIXME See FIXME at top of file 326 * 327 */ 328 private void _connect(String strUser, String strHost, String strIdentity) 329 throws IllegalActionException { 330 331 _debug("Connecting with " + strUser + "@" + strHost 332 + " with identity: " + strIdentity); 333 334 try { 335 336 strIdentity = strIdentity.trim(); 337 if (!strIdentity.equals("")) { 338 if (_setIdentities.add(strIdentity)) { 339 // we haven't seen this identity before 340 _jsch.addIdentity(strIdentity); 341 } 342 } 343 344 if (!strUser.equals(_strOldUser) || !strHost.equals(_strOldHost) 345 || !_session.isConnected()) { 346 347 if (null != _session && _session.isConnected()) { 348 _disconnect(); 349 } 350 351 _session = _jsch.getSession(strUser, strHost, 22); 352 _strOldUser = strUser; 353 _strOldHost = strHost; 354 355 // username and passphrase will be given via UserInfo interface. 356 UserInfo ui = new MyUserInfo(); 357 _session.setUserInfo(ui); 358 _session.connect(30000); 359 } 360 361 } catch (Exception e) { 362 // a couple of possible exception messages that could happen here: 363 // 1. java.io.FileNotFoundException 364 // 2. session is down 365 System.err.println("Exception caught in " + this.getFullName()); 366 System.err.println("I was trying to connect with " + strUser + "@" 367 + strHost + " with identity: " + strIdentity); 368 e.printStackTrace(); 369 throw new IllegalActionException("Exception caught in " 370 + this.getFullName() + "\n(" + e.getClass().getName() 371 + ")\n" + e.getMessage()); 372 } 373 } 374 375 /** 376 * Connect with password 1. When connected for the first time, it will 377 * prompt for password. 2. When execute for the same user@host, it should 378 * use the stored password. 3. When connect to a different user@host, it can 379 * prompt password again. 380 * 381 * @throws IllegalActionException 382 * If the connection fails. 383 * 384 * FIXME See FIXME at top of file 385 * 386 */ 387 private void _connect(String strUser, String strHost) 388 throws IllegalActionException { 389 390 _debug("Connecting with " + strUser + "@" + strHost + " with password."); 391 392 try { 393 394 if (!strUser.equals(_strOldUser) || !strHost.equals(_strOldHost) 395 || !_session.isConnected()) { 396 397 if (null != _session && _session.isConnected()) { 398 _disconnect(); 399 } 400 401 _session = _jsch.getSession(strUser, strHost, 22); 402 _strOldUser = strUser; 403 _strOldHost = strHost; 404 405 // username and passphrase will be given via UserInfo interface. 406 407 // check whether ui is already set 408 UserInfo ui; 409 ui = (UserInfo) hash.get(strUser + "@" + strHost); 410 if (ui == null) { 411 ui = new MyUserInfo(); 412 } 413 414 // If it is already there. We will use that. 415 // Hopefully we can use the info for connect to the 416 // same user@host 417 418 _session.setUserInfo(ui); 419 _session.connect(); 420 // add to the hashtable 421 hash.put(strUser + "@" + strHost, ui); 422 423 } 424 425 } catch (Exception e) { 426 // a couple of possible exception messages that could happen here: 427 // 1. java.io.FileNotFoundException 428 // 2. session is down 429 System.err.println("Exception caught in " + this.getFullName()); 430 System.err.println("I was trying to connect with " + strUser + "@" 431 + strHost + " with password."); 432 e.printStackTrace(); 433 throw new IllegalActionException("Exception caught in " 434 + this.getFullName() + "\n(" + e.getClass().getName() 435 + ")\n" + e.getMessage()); 436 } 437 } 438 439 /** 440 * 441 * @throws IllegalActionException 442 * if disconnect fails. 443 * FIXME See FIXME at top of file 444 */ 445 private void _disconnect() throws IllegalActionException { 446 if (null == _session) { 447 // no session, so nothing to disconnect 448 return; 449 } 450 451 try { 452 _session.disconnect(); 453 } catch (Exception e) { 454 System.err.println("Exception caught in " + this.getFullName()); 455 e.printStackTrace(); 456 throw new IllegalActionException("Exception caught in " 457 + this.getFullName() + "\n(" + e.getClass().getName() 458 + ")\n" + e.getMessage()); 459 } 460 } 461 462 /** 463 * 464 * @throws IllegalActionException 465 */ 466 private void scpTo(String lfile, String rfile) 467 throws IllegalActionException { 468 if (null == _session) { 469 // no session, so way to execute 470 return; 471 } 472 473 try { 474 475 // exec 'scp -t rfile' remotely 476 String command = "scp -p -t " + rfile; 477 ChannelExec channel = (ChannelExec) _session.openChannel("exec"); 478 ((ChannelExec) channel).setCommand(command); 479 480 // get I/O streams for remote scp 481 OutputStream out = channel.getOutputStream(); 482 InputStream in = channel.getInputStream(); 483 InputStream err = channel.getErrStream(); 484 485 channel.connect(); 486 487 if (checkAck(in) != 0) { 488 throw new IllegalActionException(this, "Acknowledgment error " 489 + _ackError); 490 } 491 492 // send "C0644 filesize filename", where filename should not include 493 // '/' 494 int filesize = (int) (new File(lfile)).length(); 495 command = "C0644 " + filesize + " "; 496 if (lfile.lastIndexOf('/') > 0) { 497 command += lfile.substring(lfile.lastIndexOf('/') + 1); 498 } else { 499 command += lfile; 500 } 501 command += "\n"; 502 out.write(command.getBytes()); 503 out.flush(); 504 505 if (checkAck(in) != 0) { 506 throw new IllegalActionException(this, "Acknowledgment error " 507 + _ackError); 508 } 509 510 // send a content of lfile 511 FileInputStream fis = new FileInputStream(lfile); 512 byte[] buf = new byte[1024]; 513 while (true) { 514 int len = fis.read(buf, 0, buf.length); 515 if (len <= 0) 516 break; 517 out.write(buf, 0, len); 518 out.flush(); 519 } 520 521 // send '\0' 522 buf[0] = 0; 523 out.write(buf, 0, 1); 524 out.flush(); 525 526 if (checkAck(in) != 0) { 527 throw new IllegalActionException(this, "Acknowledgment error " 528 + _ackError); 529 } 530 531 int ec = channel.getExitStatus(); 532 channel.disconnect(); 533 534 stdout.send(0, new StringToken(rfile)); 535 stderr.send(0, new StringToken(err.toString())); 536 returncode.send(0, new IntToken(ec)); 537 errors.send(0, new StringToken("")); 538 539 } catch (Exception e) { 540 System.err.println("Exception caught in " + this.getFullName()); 541 e.printStackTrace(); 542 throw new IllegalActionException("Exception caught in " 543 + this.getFullName() + "\n(" + e.getClass().getName() 544 + ")\n" + e.getMessage()); 545 } 546 } 547 548 private void scpFrom(String lfile, String rfile) 549 throws IllegalActionException { 550 if (null == _session) { 551 // no session, so way to execute 552 return; 553 } 554 555 try { 556 557 String prefix = null; 558 File localFile = new File(lfile); 559 if (localFile.isDirectory()) { 560 prefix = lfile + File.separator; 561 } 562 563 // exec 'scp -f rfile' remotely 564 String command = "scp -f " + rfile; 565 ChannelExec channel = (ChannelExec) _session.openChannel("exec"); 566 ((ChannelExec) channel).setCommand(command); 567 568 // get I/O streams for remote scp 569 OutputStream out = channel.getOutputStream(); 570 InputStream in = channel.getInputStream(); 571 InputStream err = channel.getErrStream(); 572 573 channel.connect(); 574 575 byte[] buf = new byte[1024]; 576 577 // send '\0' 578 buf[0] = 0; 579 out.write(buf, 0, 1); 580 out.flush(); 581 582 while (true) { 583 int c = checkAck(in); 584 if (c != 'C') { 585 break; 586 } 587 588 // read '0644 ' 589 in.read(buf, 0, 5); 590 591 int filesize = 0; 592 while (true) { 593 in.read(buf, 0, 1); 594 if (buf[0] == ' ') 595 break; 596 filesize = filesize * 10 + (buf[0] - '0'); 597 } 598 599 String file = null; 600 for (int i = 0;; i++) { 601 in.read(buf, i, 1); 602 if (buf[i] == (byte) 0x0a) { 603 file = new String(buf, 0, i); 604 break; 605 } 606 } 607 608 // send '\0' 609 buf[0] = 0; 610 out.write(buf, 0, 1); 611 out.flush(); 612 613 // read a content of lfile 614 FileOutputStream fos = new FileOutputStream( 615 prefix == null ? lfile : prefix + file); 616 int foo; 617 while (true) { 618 if (buf.length < filesize) 619 foo = buf.length; 620 else 621 foo = filesize; 622 in.read(buf, 0, foo); 623 fos.write(buf, 0, foo); 624 filesize -= foo; 625 if (filesize == 0) 626 break; 627 } 628 fos.close(); 629 630 if (checkAck(in) != 0) { 631 throw new IllegalActionException(this, 632 "Acknowledgment error " + _ackError); 633 } 634 635 // send '\0' 636 buf[0] = 0; 637 out.write(buf, 0, 1); 638 out.flush(); 639 } 640 641 int ec = channel.getExitStatus(); 642 channel.disconnect(); 643 644 stdout.send(0, new StringToken(localFile.getAbsolutePath())); 645 stderr.send(0, new StringToken(err.toString())); 646 returncode.send(0, new IntToken(ec)); 647 errors.send(0, new StringToken("")); 648 649 } catch (Exception e) { 650 System.err.println("Exception caught in " + this.getFullName()); 651 e.printStackTrace(); 652 throw new IllegalActionException("Exception caught in " 653 + this.getFullName() + "\n(" + e.getClass().getName() 654 + ")\n" + e.getMessage()); 655 } 656 } 657 658 private int checkAck(InputStream in) throws IOException { 659 int b = in.read(); 660 // b may be 0 for success, 661 // 1 for error, 662 // 2 for fatal error, 663 // -1 664 if (b == 0) 665 return b; 666 if (b == -1) 667 return b; 668 669 if (b == 1 || b == 2) { 670 StringBuffer sb = new StringBuffer(); 671 int c; 672 do { 673 c = in.read(); 674 sb.append((char) c); 675 } while (c != '\n'); 676 if (b == 1) { // error 677 _ackError = sb.toString(); 678 } 679 if (b == 2) { // fatal error 680 _ackError = sb.toString(); 681 } 682 } 683 return b; 684 } 685 686 // //////////////// Private variables /////////////////////// 687 688 private JSch _jsch = null; 689 private Session _session = null; 690 private HashSet<String> _setIdentities = null; 691 private String _strOldUser = null; 692 private String _strOldHost = null; 693 private String _ackError = ""; 694 // ////////////////Public Static /////////////////////// 695 // Used to store connection info 696 public static Hashtable<String,UserInfo> hash = new Hashtable<String,UserInfo>(); 697 698 // //////////////// Inner classes /////////////////////// 699 700 public static class MyUserInfo implements UserInfo { 701 702 @Override 703 public String getPassword() { 704 return passwd; 705 } 706 707 String passwd = null; 708 JTextField passwordField = (JTextField) new JPasswordField(20); 709 710 @Override 711 public boolean promptYesNo(String str) { 712 // This method gets called to answer the question similar to 713 // "are you sure you want to connect to host whose key 714 // is not in database ..." 715 return true; 716 } 717 718 @Override 719 public String getPassphrase() { 720 return null; 721 } 722 723 @Override 724 public boolean promptPassphrase(String message) { 725 return false; 726 } 727 728 @Override 729 public boolean promptPassword(String message) { 730 if (passwd != null) { 731 return true; 732 } 733 734 Object[] ob = { passwordField }; 735 int result = JOptionPane.showConfirmDialog(null, ob, message, 736 JOptionPane.OK_CANCEL_OPTION); 737 if (result == JOptionPane.OK_OPTION) { 738 passwd = passwordField.getText(); 739 return true; 740 } else { 741 return false; 742 } 743 } 744 745 @Override 746 public void showMessage(String message) { 747 // This method gets called when the server sends over a MOTD. 748 // MessageHandler(message); 749 } 750 } 751 752}