/*
** Module   : SSH2Transport.java
** Abstract : establish an SSH2-based communications channel 
**
** Copyright (c) 2008-2023, Golden Code Development Corporation.
**
** -#- -I- --Date-- ---------------------------------------Description----------------------------------------
** 001 GES 20090123 Created initial version. Provides an SSH2-based communications channel.
** 002 CA  20210827 Refactored to allow other test types, beside terminal (which can be executed from the same 
**                  test suite).  Added SOAP and REST web service tests.
** 003 TJD 20220808 Logging of ssh errors
** 004 TJD 20230110 To prevent data loss Input/Output streams must be created before connecting a channel
** 005 RFB 20230111 Added finer logging which helps with debug (plus review).
** 006 TJD 20230119 Implemented trace log level
*/

/*
** This program is free software: you can redistribute it and/or modify
** it under the terms of the GNU Affero General Public License as
** published by the Free Software Foundation, either version 3 of the
** License, or (at your option) any later version.
**
** This program is distributed in the hope that it will be useful,
** but WITHOUT ANY WARRANTY; without even the implied warranty of
** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
** GNU Affero General Public License for more details.
**
** You should have received a copy of the GNU Affero General Public License
** along with this program.  If not, see <http://www.gnu.org/licenses/>.
*/

package com.goldencode.harness.transport;

import java.io.*;
import com.jcraft.jsch.*;
import com.goldencode.util.*;
import com.goldencode.harness.*;

/**
 * Provides an SSH2-based communications channel using the JSCH project.
 */
class SSH2Transport
implements Connection
{
   /** JSCH session object used to obtaining a channel. */
   private Session session = null;
   
   /** JSCH channel that provides streams over SSH2. */
   private ChannelShell channel = null;
   
   /** JSCH channel output stream. */
   private OutputStream os = null;

   /** JSCH channel input stream. */
   private InputStream is = null;

   /**
    * Connect to the defined system/account and initialize a working
    * communications session.
    *
    * @param    cfg
    *           Connection configuration.
    * @param    rc
    *           Object in which to store the details of failures.
    *
    * @return   <code>true</code> if the connection was successfully made
    *           and is now usable. <code>false</code> on any failure.
    */
   public synchronized boolean connect(TerminalSessionSettings cfg, FailureNotification rc)
   {
      if (channel == null)
      {
         try
         {
            JSch jsch = new JSch();
            
            session = jsch.getSession(cfg.userid, cfg.host, cfg.port);
            session.setPassword(cfg.password);
            session.setConfig("StrictHostKeyChecking", "no");
            if (LogHelper.isFinestLoggable())
            {
               LogHelper.finest("Connect id=" + cfg.userid + " host=" + cfg.host + " port=" + cfg.port);
            }
            session.connect(30000);
            
            if (LogHelper.isFinestLoggable())
            {
               LogHelper.finest("connect attempt completed. session=" + session.toString());
            }
            channel = (ChannelShell) session.openChannel("shell");

            if (channel == null)
            {
               rc.setResult(ExitCode.CONNECT_FAILURE);
               rc.setDetails("Channel is null");
               LogHelper.warn("Channel is null");
               // cleanup
               disconnect();
               return false;
            }
            channel.setPtyType(cfg.termType,
                               cfg.termColumns,
                               cfg.termRows,
                               0,
                               0);
            // streams need to be open before channel is connected to prevent data loss
            os = channel.getOutputStream();
            is = channel.getInputStream();
            channel.connect(3*1000);
            if (LogHelper.isFinestLoggable())
            {
               LogHelper.finest("channel completed isConnected=" + session.isConnected());
            }
         }
         catch (IOException e)
         {
            rc.setResult(ExitCode.CONNECT_FAILURE);
            rc.setDetails(LogHelper.dumpThrowable(e));
            LogHelper.warn("IOException ", e);
            disconnect();
            return false;
         }
         catch (JSchException je)
         {
            rc.setResult(ExitCode.CONNECT_FAILURE);
            rc.setDetails(LogHelper.dumpThrowable(je));
            LogHelper.warn("JSchException ", je);
            disconnect();
            return false;
         }
      }
      
      return true;
   }
   
   /**
    * Disconnect the currently working communications session.
    */
   public synchronized void disconnect()
   {
      is = null; 
      os = null;
      if (channel != null)
      {
         if (channel.isConnected())
         {
            channel.disconnect();
         }
         channel = null;
      }
      
      if (session != null)
      {
         if (session.isConnected())
         {
            session.disconnect();
         }
         session = null;
      }
   }
   
   /**
    * Return the output destination for the currently working communications
    * session.
    *
    * @param    rc
    *           Object in which to store the details of failures.
    *
    * @return   The stream to be written to or <code>null</code> if the
    *           communications session is invalid or not active.
    */
   public synchronized OutputStream getOutputStream(FailureNotification rc)
   {
      if (os == null)
      {
         rc.setResult(ExitCode.CONNECT_FAILURE);
         rc.setDetails("Channel OutputStream is null");
         LogHelper.warn("Channel OutputStream is null ");
      }
      
      return os; 
   }
   
   /**
    * Return the input source for the currently working communications
    * session.
    *
    * @param    rc
    *           Object in which to store the details of failures.
    *
    * @return   The stream to be read from or <code>null</code> if the
    *           communications session is invalid or not active.
    */
   public synchronized InputStream getInputStream(FailureNotification rc)
   {
      if (is == null)
      {
         rc.setResult(ExitCode.CONNECT_FAILURE);
         rc.setDetails("Channel OutputStream is null");
         LogHelper.warn("Channel OutputStream is null ");
      }
      
      return is; 
   }
}
