content: /*
   *  Licensed to the Apache Software Foundation (ASF) under one or more
   *  contributor license agreements.  See the NOTICE file distributed with
   *  this work for additional information regarding copyright ownership.
   *  The ASF licenses this file to You under the Apache License, Version 2.0
   *  (the "License"); you may not use this file except in compliance with
   *  the License.  You may obtain a copy of the License at
   *
   *      https://www.apache.org/licenses/LICENSE-2.0
   *
   *  Unless required by applicable law or agreed to in writing, software
   *  distributed under the License is distributed on an "AS IS" BASIS,
   *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
   *  See the License for the specific language governing permissions and
   *  limitations under the License.
   *
   */
  package org.apache.ivy.plugins.repository.ssh;
  
  import java.io.BufferedInputStream;
  import java.io.BufferedOutputStream;
  import java.io.File;
  import java.io.FileInputStream;
  import java.io.FileOutputStream;
  import java.io.IOException;
  import java.io.InputStream;
  import java.io.OutputStream;
  
  import com.jcraft.jsch.Channel;
  import com.jcraft.jsch.ChannelExec;
  import com.jcraft.jsch.JSchException;
  import com.jcraft.jsch.Session;
  
  /**
   * This class is using the scp client to transfer data and information for the repository.
   * <p>
   * It is based on the SCPClient from the ganymed ssh library from Christian Plattner, released under
   * a BSD style license.
   * <p>
   * To minimize the dependency to the ssh library and because we needed some additional
   * functionality, we decided to copy'n'paste the single class rather than to inherit or delegate it
   * somehow.
   * <p>
   * Nevertheless credit should go to the original author.
   */
  public class Scp {
      private static final int MODE_LENGTH = 4;
  
      private static final int SEND_FILE_BUFFER_LENGTH = 40000;
  
      private static final int SEND_BYTES_BUFFER_LENGTH = 512;
  
      private static final int MIN_TLINE_LENGTH = 8;
  
      private static final int CLINE_SPACE_INDEX2 = 5;
  
      private static final int CLINE_SPACE_INDEX1 = 4;
  
      private static final int MIN_C_LINE_LENGTH = 8;
  
      private static final int DEFAULT_LINE_BUFFER_LENGTH = 30;
  
      private static final int BUFFER_SIZE = 64 * 1024;
  
      /*
       * Maximum length authorized for scp lines. This is a random limit - if your path names are
       * longer, then adjust it.
       */
      private static final int MAX_SCP_LINE_LENGTH = 8192;
  
      private Session session;
  
      public class FileInfo {
          private String filename;
  
          private long length;
  
          private long lastModified;
  
          /**
           * @param filename
           *            The filename to set.
           */
          public void setFilename(String filename) {
              this.filename = filename;
          }
  
          /**
           * @return Returns the filename.
           */
          public String getFilename() {
              return filename;
          }
  
          /**
           * @param length
           *            The length to set.
           */
          public void setLength(long length) {
              this.length = length;
          }
  
          /**
           * @return Returns the length.
           */
          public long getLength() {
              return length;
          }
  
          /**
           * @param lastModified
           *            The lastModified to set.
           */
          public void setLastModified(long lastModified) {
              this.lastModified = lastModified;
          }
  
          /**
           * @return Returns the lastModified.
           */
          public long getLastModified() {
              return lastModified;
          }
      }
  
      public Scp(Session session) {
          if (session == null) {
              throw new IllegalArgumentException("Cannot accept null argument!");
          }
          this.session = session;
      }
  
      private void readResponse(InputStream is) throws IOException, RemoteScpException {
          int c = is.read();
  
          if (c == 0) {
              return;
          }
  
          if (c == -1) {
              throw new RemoteScpException("Remote scp terminated unexpectedly.");
          }
  
          if (c != 1 && c != 2) {
              throw new RemoteScpException("Remote scp sent illegal error code.");
          }
  
          if (c == 2) {
              throw new RemoteScpException("Remote scp terminated with error.");
          }
  
          String err = receiveLine(is);
          throw new RemoteScpException("Remote scp terminated with error (" + err + ").");
      }
  
      private String receiveLine(InputStream is) throws IOException, RemoteScpException {
          StringBuilder sb = new StringBuilder(DEFAULT_LINE_BUFFER_LENGTH);
  
          while (true) {
  
              if (sb.length() > MAX_SCP_LINE_LENGTH) {
                  throw new RemoteScpException("Remote scp sent a too long line");
              }
  
              int c = is.read();
  
              if (c < 0) {
                  throw new RemoteScpException("Remote scp terminated unexpectedly.");
              }
  
              if (c == '\n') {
                  break;
              }
  
              sb.append((char) c);
  
          }
          return sb.toString();
      }
  
      private void parseCLine(String line, FileInfo fileInfo) throws RemoteScpException {
          /* Minimum line: "xxxx y z" ---> 8 chars */
  
          long len;
  
          if (line.length() < MIN_C_LINE_LENGTH) {
              throw new RemoteScpException(
                      "Malformed C line sent by remote SCP binary, line too short.");
          }
  
          if (line.charAt(CLINE_SPACE_INDEX1) != ' ' || line.charAt(CLINE_SPACE_INDEX2) == ' ') {
              throw new RemoteScpException("Malformed C line sent by remote SCP binary.");
          }
  
          int lengthNameSep = line.indexOf(' ', CLINE_SPACE_INDEX2);
  
          if (lengthNameSep == -1) {
              throw new RemoteScpException("Malformed C line sent by remote SCP binary.");
          }
  
          String lengthSubstring = line.substring(CLINE_SPACE_INDEX2, lengthNameSep);
          String nameSubstring = line.substring(lengthNameSep + 1);
  
          if (lengthSubstring.length() <= 0 || nameSubstring.length() <= 0) {
              throw new RemoteScpException("Malformed C line sent by remote SCP binary.");
          }
  
          if (CLINE_SPACE_INDEX2 + 1 + lengthSubstring.length() + nameSubstring.length() != line
                  .length()) {
              throw new RemoteScpException("Malformed C line sent by remote SCP binary.");
          }
  
          try {
              len = Long.parseLong(lengthSubstring);
          } catch (NumberFormatException e) {
              throw new RemoteScpException(
                      "Malformed C line sent by remote SCP binary, cannot parse file length.");
          }
  
          if (len < 0) {
              throw new RemoteScpException(
                      "Malformed C line sent by remote SCP binary, illegal file length.");
          }
  
          fileInfo.setLength(len);
          fileInfo.setFilename(nameSubstring);
      }
  
      private void parseTLine(String line, FileInfo fileInfo) throws RemoteScpException {
          /* Minimum line: "0 0 0 0" ---> 8 chars */
  
          long modtime;
          long firstMsec;
          long atime;
          long secondMsec;
  
          if (line.length() < MIN_TLINE_LENGTH) {
              throw new RemoteScpException(
                      "Malformed T line sent by remote SCP binary, line too short.");
          }
  
          int firstMsecBegin = line.indexOf(" ") + 1;
          if (firstMsecBegin == 0 || firstMsecBegin >= line.length()) {
              throw new RemoteScpException(
                      "Malformed T line sent by remote SCP binary, line not enough data.");
          }
  
          int atimeBegin = line.indexOf(" ", firstMsecBegin + 1) + 1;
          if (atimeBegin == 0 || atimeBegin >= line.length()) {
              throw new RemoteScpException(
                      "Malformed T line sent by remote SCP binary, line not enough data.");
          }
  
          int secondMsecBegin = line.indexOf(" ", atimeBegin + 1) + 1;
          if (secondMsecBegin == 0 || secondMsecBegin >= line.length()) {
              throw new RemoteScpException(
                      "Malformed T line sent by remote SCP binary, line not enough data.");
          }
  
          try {
              modtime = Long.parseLong(line.substring(0, firstMsecBegin - 1));
              firstMsec = Long.parseLong(line.substring(firstMsecBegin, atimeBegin - 1));
              atime = Long.parseLong(line.substring(atimeBegin, secondMsecBegin - 1));
              secondMsec = Long.parseLong(line.substring(secondMsecBegin));
          } catch (NumberFormatException e) {
              throw new RemoteScpException(
                      "Malformed C line sent by remote SCP binary, cannot parse file length.");
          }
  
          if (modtime < 0 || firstMsec < 0 || atime < 0 || secondMsec < 0) {
              throw new RemoteScpException(
                      "Malformed C line sent by remote SCP binary, illegal file length.");
          }
  
          fileInfo.setLastModified(modtime);
      }
  
      private void sendFile(Channel channel, String localFile, String remoteName, String mode)
              throws IOException, RemoteScpException {
          byte[] buffer = new byte[BUFFER_SIZE];
  
          OutputStream os = new BufferedOutputStream(channel.getOutputStream(),
                  SEND_FILE_BUFFER_LENGTH);
          InputStream is = new BufferedInputStream(channel.getInputStream(), SEND_BYTES_BUFFER_LENGTH);
  
          try {
              if (channel.isConnected()) {
                  channel.start();
              } else {
                  channel.connect();
              }
          } catch (JSchException jsche) {
              throw new IOException("Channel connection problems", jsche);
          }
  
          readResponse(is);
  
          File f = new File(localFile);
          long remain = f.length();
  
          String cMode = mode;
          if (cMode == null) {
              cMode = "0600";
          }
          String cline = "C" + cMode + " " + remain + " " + remoteName + "\n";
  
          os.write(cline.getBytes());
          os.flush();
  
          readResponse(is);
  
          try (FileInputStream fis = new FileInputStream(f)) {
              while (remain > 0) {
                  int trans;
                  if (remain > buffer.length) {
                      trans = buffer.length;
                  } else {
                      trans = (int) remain;
                  }
                  if (fis.read(buffer, 0, trans) != trans) {
                      throw new IOException("Cannot read enough from local file " + localFile);
                  }
  
                  os.write(buffer, 0, trans);
  
                  remain -= trans;
              }
          }
  
          os.write(0);
          os.flush();
  
          readResponse(is);
  
          os.write("E\n".getBytes());
          os.flush();
      }
  
      /**
       * Receive a file via scp and store it in a stream
       *
       * @param channel
       *            ssh channel to use
       * @param file
       *            to receive from remote
       * @param targetStream
       *            to store file into (if null, get only file info)
       * @return file information of the file we received
       * @throws IOException
       *             in case of network or protocol trouble
       * @throws RemoteScpException
       *             in case of problems on the target system (connection is fine)
       */
      private FileInfo receiveStream(Channel channel, String file, OutputStream targetStream)
              throws IOException, RemoteScpException {
          byte[] buffer = new byte[BUFFER_SIZE];
  
          OutputStream os = channel.getOutputStream();
          InputStream is = channel.getInputStream();
          try {
              if (channel.isConnected()) {
                  channel.start();
              } else {
                  channel.connect();
              }
          } catch (JSchException jsche) {
              throw new IOException("Channel connection problems", jsche);
          }
          os.write(0x0);
          os.flush();
  
          FileInfo fileInfo = new FileInfo();
  
          while (true) {
              int c = is.read();
              if (c < 0) {
                  throw new RemoteScpException("Remote scp terminated unexpectedly.");
              }
  
              String line = receiveLine(is);
  
              if (c == 'T') {
                  parseTLine(line, fileInfo);
                  os.write(0x0);
                  os.flush();
                  continue;
              }
              if (c == 1 || c == 2) {
                  throw new RemoteScpException("Remote SCP error: " + line);
              }
  
              if (c == 'C') {
                  parseCLine(line, fileInfo);
                  break;
              }
              throw new RemoteScpException("Remote SCP error: " + ((char) c) + line);
          }
          if (targetStream != null) {
  
              os.write(0x0);
              os.flush();
  
              try {
                  long remain = fileInfo.getLength();
  
                  while (remain > 0) {
                      int trans;
                      if (remain > buffer.length) {
                          trans = buffer.length;
                      } else {
                          trans = (int) remain;
                      }
  
                      int thisTimeReceived = is.read(buffer, 0, trans);
  
                      if (thisTimeReceived < 0) {
                          throw new IOException("Remote scp terminated connection unexpectedly");
                      }
  
                      targetStream.write(buffer, 0, thisTimeReceived);
  
                      remain -= thisTimeReceived;
                  }
  
                  targetStream.close();
              } catch (IOException e) {
                  if (targetStream != null) {
                      targetStream.close();
                  }
                  throw (e);
              }
  
              readResponse(is);
  
              os.write(0x0);
              os.flush();
          }
          return fileInfo;
      }
  
      /**
       * @return ChannelExec
       * @throws JSchException if something goes wrong
       */
      private ChannelExec getExecChannel() throws JSchException {
          ChannelExec channel;
          channel = (ChannelExec) session.openChannel("exec");
          return channel;
      }
  
      /**
       * Copy a local file to a remote site, uses the specified mode when creating the file on the
       * remote side.
       *
       * @param localFile
       *            Path and name of local file. Must be absolute.
       * @param remoteTargetDir
       *            Remote target directory where the file has to end up (optional)
       * @param remoteTargetName
       *            file name to use on the target system
       * @param mode
       *            a four digit string (e.g., 0644, see "man chmod", "man open")
       * @throws IOException
       *             in case of network problems
       * @throws RemoteScpException
       *             in case of problems on the target system (connection ok)
       */
      @SuppressWarnings("unused")
      public void put(String localFile, String remoteTargetDir, String remoteTargetName, String mode)
              throws IOException, RemoteScpException {
          ChannelExec channel = null;
  
          if (localFile == null || remoteTargetName == null) {
              throw new IllegalArgumentException("Null argument.");
          }
  
          if (mode != null) {
              if (mode.length() != MODE_LENGTH) {
                  throw new IllegalArgumentException("Invalid mode.");
              }
  
              for (char c : mode.toCharArray()) {
                  if (!Character.isDigit(c)) {
                      throw new IllegalArgumentException("Invalid mode.");
                  }
              }
          }
  
          String cmd = "scp -t ";
          if (mode != null) {
              cmd += "-p ";
          }
          if (remoteTargetDir != null && remoteTargetDir.length() > 0) {
              cmd += "-d " + remoteTargetDir;
          }
  
          try {
              channel = getExecChannel();
              channel.setCommand(cmd);
              sendFile(channel, localFile, remoteTargetName, mode);
              channel.disconnect();
          } catch (JSchException e) {
              if (channel != null) {
                  channel.disconnect();
              }
              throw new IOException("Error during SCP transfer." + e.getMessage(), e);
          }
      }
  
      /**
       * Download a file from the remote server to a local file.
       *
       * @param remoteFile
       *            Path and name of the remote file.
       * @param localTarget
       *            Local file where to store the data. Must be absolute.
       * @throws IOException
       *             in case of network problems
       * @throws RemoteScpException
       *             in case of problems on the target system (connection ok)
       */
      public void get(String remoteFile, String localTarget) throws IOException, RemoteScpException {
          File f = new File(localTarget);
          FileOutputStream fop = new FileOutputStream(f);
          get(remoteFile, fop);
      }
  
      /**
       * Download a file from the remote server into an OutputStream
       *
       * @param remoteFile
       *            Path and name of the remote file.
       * @param localTarget
       *            OutputStream to store the data.
       * @throws IOException
       *             in case of network problems
       * @throws RemoteScpException
       *             in case of problems on the target system (connection ok)
       */
      @SuppressWarnings("unused")
      public void get(String remoteFile, OutputStream localTarget) throws IOException,
              RemoteScpException {
          ChannelExec channel = null;
  
          if (remoteFile == null || localTarget == null) {
              throw new IllegalArgumentException("Null argument.");
          }
  
          String cmd = "scp -p -f " + remoteFile;
  
          try {
              channel = getExecChannel();
              channel.setCommand(cmd);
              receiveStream(channel, remoteFile, localTarget);
              channel.disconnect();
          } catch (JSchException e) {
              if (channel != null) {
                  channel.disconnect();
              }
              throw new IOException("Error during SCP transfer. " + e.getMessage(), e);
          }
      }
  
      /**
       * Initiates an SCP sequence but stops after getting fileinformation header
       *
       * @param remoteFile
       *            to get information for
       * @return the file information got
       * @throws IOException
       *             in case of network problems
       * @throws RemoteScpException
       *             in case of problems on the target system (connection ok)
       */
      public FileInfo getFileinfo(String remoteFile) throws IOException, RemoteScpException {
          ChannelExec channel = null;
          FileInfo fileInfo = null;
  
          if (remoteFile == null) {
              throw new IllegalArgumentException("Null argument.");
          }
  
          String cmd = "scp -p -f \"" + remoteFile + "\"";
  
          try {
              channel = getExecChannel();
              channel.setCommand(cmd);
              fileInfo = receiveStream(channel, remoteFile, null);
              channel.disconnect();
          } catch (JSchException e) {
              throw new IOException("Error during SCP transfer. " + e.getMessage(), e);
          } finally {
              if (channel != null) {
                  channel.disconnect();
              }
          }
          return fileInfo;
      }
  }
  