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}