/***************************************************************************
 *   fqterm, a terminal emulator for both BBS and *nix.                    *
 *   Copyright (C) 2008 fqterm development group.                          *
 *                                                                         *
 *   This program is free software; you can redistribute it and/or modify  *
 *   it under the terms of the GNU General Public License as published by  *
 *   the Free Software Foundation; either version 2 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 General Public License for more details.                          *
 *                                                                         *
 *   You should have received a copy of the GNU General Public License     *
 *   along with this program; if not, write to the                         *
 *   Free Software Foundation, Inc.,                                       *
 *   51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.               *
 ***************************************************************************/

#include "fqterm_ssh_channel.h"
#include "fqterm_ssh_const.h"
#include "fqterm_ssh_packet.h"
#include "fqterm_trace.h"
#include <QString>
namespace FQTerm {

//==============================================================================
//FQTermSSH1Channel
//==============================================================================

FQTermSSH1Channel::FQTermSSH1Channel()
    : FQTermSSHChannel() {
  service_state_ = FQTermSSH1Channel::BEGIN_SERVICE;
}

void FQTermSSH1Channel::initChannel(FQTermSSHPacketReceiver *packet,
                                    FQTermSSHPacketSender *output,
                                    int col, int row, const QString& termtype) {
  packet_receiver_ = packet;
  packet_sender_ = output;
  packet_receiver_->disconnect(this);
  FQ_VERIFY(connect(packet_receiver_, SIGNAL(packetAvaliable(int)), this, SLOT(handlePacket(int))));
  packet_sender_->startPacket(SSH1_CMSG_REQUEST_PTY);
  // pty request is of no use in BBS, but we do this
  packet_sender_->putString(termtype.toLatin1());
  packet_sender_->putInt(row);  // FIXME: hardcoded term size.
  packet_sender_->putInt(col);
  packet_sender_->putInt(0);
  packet_sender_->putInt(0);
  packet_sender_->putByte(0);
  packet_sender_->write();
  service_state_ = FQTermSSH1Channel::REQPTY_SENT;

  is_closed_ = false;
}

void FQTermSSH1Channel::changeTermSize(int col, int row) {
  packet_sender_->startPacket(SSH1_CMSG_WINDOW_SIZE);
  packet_sender_->putInt(row);
  packet_sender_->putInt(col);
  packet_sender_->putInt(0);
  packet_sender_->putInt(0);
  packet_sender_->write();
}

void FQTermSSH1Channel::sendData(const char *data, int len) {
  packet_sender_->startPacket(SSH1_CMSG_STDIN_DATA);
  packet_sender_->putInt(len);
  packet_sender_->putRawData(data, len);
  packet_sender_->write();
}

void FQTermSSH1Channel::closeConnection(const char *reason) {
  packet_sender_->startPacket(SSH1_MSG_DISCONNECT);
  packet_sender_->putString(reason);
  packet_sender_->write();
  is_closed_ = true;
}

void FQTermSSH1Channel::handlePacket(int type) {
  switch (service_state_) {
    case BEGIN_SERVICE:
      FQ_TRACE("sshchannel", 0) << "Channel: We should not be here";
      break;
    case REQPTY_SENT:
      if (type != SSH1_SMSG_SUCCESS) {
        emit channelError(tr("Server refused pty allocation!"));
      }
      packet_sender_->startPacket(SSH1_CMSG_EXEC_SHELL);
      packet_sender_->write();
      emit channelOK();
      service_state_ = FQTermSSH1Channel::SERVICE_OK;
      //emit msg to tell window we could process input.
      break;
    case SERVICE_OK:
      switch (type) {
        case SSH1_SMSG_STDOUT_DATA:
        case SSH1_SMSG_STDERR_DATA:
          {
            const char *data = (const char *)packet_receiver_->buffer_->data() + 4;
            int len = packet_receiver_->packetDataLen() - 4;
            emit channelReadyRead(data, len);
          }
          break;
        case SSH1_SMSG_X11_OPEN:
        case SSH1_SMSG_AGENT_OPEN:
        case SSH1_MSG_PORT_OPEN:
          {
            int i = packet_receiver_->getInt();
            packet_sender_->startPacket(SSH1_MSG_CHANNEL_OPEN_FAILURE);
            packet_sender_->putInt(i);
            packet_sender_->write();
          }
          break;
        case SSH1_SMSG_EXIT_STATUS:
          packet_sender_->startPacket(SSH1_CMSG_EXIT_CONFIRMATION);
          packet_sender_->write();
          closeConnection("end");
          is_closed_ = true;
          break;
        case SSH1_SMSG_SUCCESS:
        case SSH1_SMSG_FAILURE:
          break;
        default:
          FQ_TRACE("sshchannel", 0) << "Unimplemented message: "
                                    << service_state_;
      }
  }
}


//==============================================================================
//FQTermSSH2Channel
//==============================================================================
u_int32_t FQTermSSH2Channel::generateChannelID() {
  static u_int32_t id = 0;
  return id++;
}

FQTermSSH2Channel::FQTermSSH2Channel()
    : FQTermSSHChannel(),
      col_(80),
      row_(24),
      termtype_("vt100") {
  channel_id_ = generateChannelID();
  server_channel_id_ = 0xCCCCCCCC;
  local_window_size_ = 0;
  channel_state_ = FQTermSSH2Channel::BEGIN_CHANNEL;
}

void FQTermSSH2Channel::initChannel(FQTermSSHPacketReceiver *packet,
                                    FQTermSSHPacketSender *output,
                                    int col, int row, const QString& termtype) {
  FQ_FUNC_TRACE("ssh2channel", 5);
  col_ = col;
  row_ = row;
  termtype_ = termtype;
  packet_receiver_ = packet;
  packet_sender_ = output;
  packet_receiver_->disconnect(this);
  FQ_VERIFY(connect(packet_receiver_, SIGNAL(packetAvaliable(int)), this, SLOT(handlePacket(int))));

  //    byte      SSH_MSG_CHANNEL_OPEN
  //    string    channel type in US-ASCII only
  //    uint32    sender channel
  //    uint32    initial window size
  //    uint32    maximum packet size
  //    ....      channel type specific data follows

  packet_sender_->startPacket(SSH2_MSG_CHANNEL_OPEN);
  packet_sender_->putString("session");
  packet_sender_->putInt(channel_id_);
  packet_sender_->putInt(MAX_LOCAL_WINDOW_SIZE);  // TODO: what's the best window size?
  packet_sender_->putInt(MAX_LOCAL_PACKET_SIZE);  // TODO: what's the best maximum packet size?
  packet_sender_->write();

  server_window_size_ = 0;
  server_max_packet_size_ = 0;
  local_window_size_ = MAX_LOCAL_WINDOW_SIZE;

  is_closed_ = false;
}

void FQTermSSH2Channel::changeTermSize(int col, int row) {
  //    byte      SSH_MSG_CHANNEL_REQUEST
  //    uint32    recipient channel
  //    string    "window-change"
  //    boolean   FALSE
  //    uint32    terminal width, columns
  //    uint32    terminal height, rows
  //    uint32    terminal width, pixels
  //    uint32    terminal height, pixels

  packet_sender_->startPacket(SSH2_MSG_CHANNEL_REQUEST);
  packet_sender_->putInt(server_channel_id_);
  packet_sender_->putString("window-change");
  packet_sender_->putByte(false);
  packet_sender_->putInt(col);
  packet_sender_->putInt(row);
  packet_sender_->putInt(640);  // FIXME: hard-coded screen pixels.
  packet_sender_->putInt(480);
  packet_sender_->write();
}

void FQTermSSH2Channel::sendData(const char *data, int len) {
  if (len > (int)server_window_size_ || len > (int)server_max_packet_size_) {
    FQ_TRACE("ssh2channel", 3) << "Data length is greater than server's capacity: "
                               << "data length: " << len << ", "
                               << "server window: " << server_window_size_ << ", "
                               << "server packet max size: " << server_max_packet_size_;
    return;
  }

  //    byte      SSH_MSG_CHANNEL_DATA
  //    uint32    recipient channel
  //    string    data
  packet_sender_->startPacket(SSH2_MSG_CHANNEL_DATA);
  packet_sender_->putInt(server_channel_id_);
  packet_sender_->putString(data, len);
  packet_sender_->write();

  server_window_size_ -= len;;

  FQ_TRACE("ssh2channel", 5) << len
                             << " bytes data sent, server window size left: "
                             << server_window_size_;;
}

void FQTermSSH2Channel::closeConnection(const char *reason) {
  packet_sender_->startPacket(SSH1_MSG_DISCONNECT);
  packet_sender_->putString(reason);
  packet_sender_->write();
  is_closed_ = true;
}

void FQTermSSH2Channel::requestPty() {
  if (packet_receiver_->packetType() == SSH2_MSG_CHANNEL_OPEN_FAILURE) {
    // TODO: Here the error reason in the packet is ignored.
    emit channelError(tr("Server refuces to open a channel."));
    return;
  }

  if (packet_receiver_->packetType() != SSH2_MSG_CHANNEL_OPEN_CONFIRMATION) {
    emit channelError(tr("Server error when opening a channel."));
    return;
  }

  FQ_TRACE("ssh2channel", 5) << "Channel open. Try to request a pty.";

  //    byte      SSH_MSG_CHANNEL_OPEN_CONFIRMATION
  //    uint32    recipient channel
  //    uint32    sender channel
  //    uint32    initial window size
  //    uint32    maximum packet size
  //    ....      channel type specific data follows
  packet_receiver_->consume(4);
  server_channel_id_ = packet_receiver_->getInt();
  server_window_size_ = packet_receiver_->getInt();
  server_max_packet_size_ = packet_receiver_->getInt();

  //    byte      SSH_MSG_CHANNEL_REQUEST
  //    uint32    recipient channel
  //    string    "pty-req"
  //    boolean   want_reply
  //    string    TERM environment variable value (e.g., vt100)
  //    uint32    terminal width, characters (e.g., 80)
  //    uint32    terminal height, rows (e.g., 24)
  //    uint32    terminal width, pixels (e.g., 640)
  //    uint32    terminal height, pixels (e.g., 480)
  //    string    encoded terminal modes
  packet_sender_->startPacket(SSH2_MSG_CHANNEL_REQUEST);
  packet_sender_->putInt(server_channel_id_);
  packet_sender_->putString("pty-req");
  packet_sender_->putByte(true);
  packet_sender_->putString(termtype_.toLatin1()); // TODO: hardcoded term type.
  packet_sender_->putInt(col_);   // FIXME: hardcoded screen parameters.
  packet_sender_->putInt(row_);
  packet_sender_->putInt(640);
  packet_sender_->putInt(480);
  packet_sender_->putString("");  // TODO: no modes sent.
  packet_sender_->write();
}


void FQTermSSH2Channel::requestShell() {
  if (packet_receiver_->packetType() != SSH2_MSG_CHANNEL_SUCCESS) {
    emit channelError(tr("Server refused pty allocation!"));
  }
  FQ_TRACE("ssh2channel", 5) << "Pty allocated. Now try to request a shell.";
  //    byte      SSH_MSG_CHANNEL_REQUEST
  //    uint32    recipient channel
  //    string    "shell"
  //    boolean   want reply
  packet_sender_->startPacket(SSH2_MSG_CHANNEL_REQUEST);
  packet_sender_->putInt(server_channel_id_);
  packet_sender_->putString("shell");
  packet_sender_->putByte(true);
  packet_sender_->write();
}

void FQTermSSH2Channel::checkLocalWindowSize() {
  if (local_window_size_ < MAX_LOCAL_WINDOW_SIZE/2) {
    //    byte      SSH_MSG_CHANNEL_WINDOW_ADJUST
    //    uint32    recipient channel
    //    uint32    bytes to add
    int inc = MAX_LOCAL_WINDOW_SIZE/2;
    local_window_size_ += inc;

    packet_sender_->startPacket(SSH2_MSG_CHANNEL_WINDOW_ADJUST);
    packet_sender_->putInt(server_channel_id_);
    packet_sender_->putInt(inc);
    packet_sender_->write();
  }
}

void FQTermSSH2Channel::processChannelPacket() {
  switch (packet_receiver_->packetType()) {
    case SSH2_MSG_CHANNEL_WINDOW_ADJUST:
      {
        //    byte      SSH_MSG_CHANNEL_WINDOW_ADJUST
        //    uint32    recipient channel
        //    uint32    bytes to add
        packet_receiver_->consume(4);  // channel id is already checked.
        int inc = packet_receiver_->getInt();
        server_window_size_ += inc;
        FQ_TRACE("ssh2channel", 5) << "Server window size increased from "
                                   << server_window_size_ - inc
                                   << " to " << server_window_size_;
      }
      break;
    case  SSH2_MSG_CHANNEL_DATA:
      {
        //    byte      SSH_MSG_CHANNEL_DATA
        //    uint32    recipient channel
        //    string    data
        packet_receiver_->consume(4);
        int len = packet_receiver_->getInt();
        const char *data = (const char *)packet_receiver_->buffer_->data();
        local_window_size_ -= len;
        checkLocalWindowSize();
        emit channelReadyRead(data, len);
      }
      break;
    case  SSH2_MSG_CHANNEL_EXTENDED_DATA:
      {
        //    byte      SSH_MSG_CHANNEL_DATA
        //    uint32    recipient channel
        //    string    data
        packet_receiver_->consume(4);
        int len = packet_receiver_->getInt();
        const char *data = (const char *)packet_receiver_->buffer_->data();
        local_window_size_ -= len;
        checkLocalWindowSize();
        emit channelReadyRead(data, len);
      }
      break;
    case SSH2_MSG_CHANNEL_EOF:
    case SSH2_MSG_CHANNEL_CLOSE:
      //    byte      SSH_MSG_CHANNEL_EOF
      //    uint32    recipient channel
      // FIXME: this error would cause the connection closed, while only the channel need be closed in ssh2.
      emit channelError(tr("Channel closed by the server."));
      break;
    case SSH2_MSG_CHANNEL_REQUEST:
      //    byte      SSH_MSG_CHANNEL_REQUEST
      //    uint32    recipient channel
      //    string    "xon-xoff"
      //    boolean   FALSE
      //    boolean   client can do

      // TODO: just ignore this message currently.
      break;
  }
}

void FQTermSSH2Channel::handlePacket(int type) {
  // first check the channel id.
  u_int32_t channel_id = ntohu32(packet_receiver_->buffer_->data());
  if (channel_id != channel_id_) {
    return;
  }

  switch (channel_state_) {
    case FQTermSSH2Channel::BEGIN_CHANNEL:
      requestPty();
      channel_state_ = FQTermSSH2Channel::REQUEST_PTY_SENT;
      break;
    case FQTermSSH2Channel::REQUEST_PTY_SENT:
      requestShell();
      channel_state_ = FQTermSSH2Channel::REQUEST_SHELL_SENT;
      break;
    case FQTermSSH2Channel::REQUEST_SHELL_SENT:
      switch (type) {
        case SSH2_MSG_CHANNEL_REQUEST:
          {
            FQ_TRACE("ssh2channel", 8) << "SSH2_MSG_CHANNEL_REQUEST isn't supported, just send back a packet with SSH2_MSG_CHANNEL_SUCCESS if reply is needed.";

            packet_receiver_->consume(4);
            u_int32_t len = ntohu32(packet_receiver_->buffer_->data());
            packet_receiver_->consume(4 + len);
            bool replyNeeded = packet_receiver_->getByte();

            if (replyNeeded) {
              packet_sender_->startPacket(SSH2_MSG_CHANNEL_SUCCESS);
              packet_sender_->putInt(server_channel_id_);
              packet_sender_->write();
            }
          }
          break;
        case SSH2_MSG_CHANNEL_WINDOW_ADJUST:
          {
            packet_receiver_->consume(4);
            u_int32_t inc = packet_receiver_->getInt();
            server_window_size_ += inc;
          }
          break;
        case SSH2_MSG_CHANNEL_SUCCESS:
          emit channelOK();
          channel_state_ = FQTermSSH2Channel::CHANNEL_OK;
          //emit a msg to tell window we could process input.
          break;
        case SSH2_MSG_CHANNEL_FAILURE:
          emit channelError(tr("Can't open a shell."));
          break;
        default:
          emit channelError(tr("Unsupported packet."));
          break;
      }
      break;
    case FQTermSSH2Channel::CHANNEL_OK:
      processChannelPacket();
      break;
  }
}

}  // namespace FQTerm

#include "fqterm_ssh_channel.moc"
