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.tools.ant.taskdefs.optional.ssh;
  
  import java.io.File;
  import java.io.IOException;
  import java.util.ArrayList;
  import java.util.LinkedList;
  import java.util.List;
  import java.util.stream.Stream;
  
  import org.apache.tools.ant.BuildException;
  import org.apache.tools.ant.DirectoryScanner;
  import org.apache.tools.ant.Project;
  import org.apache.tools.ant.types.FileSet;
  import org.apache.tools.ant.types.Resource;
  import org.apache.tools.ant.types.ResourceCollection;
  import org.apache.tools.ant.types.resources.FileProvider;
  import org.apache.tools.ant.types.resources.FileResource;
  import org.apache.tools.ant.util.ResourceUtils;
  
  import com.jcraft.jsch.JSchException;
  import com.jcraft.jsch.Session;
  
  /**
   * Ant task for sending files to remote machine over ssh/scp.
   *
   * @since Ant 1.6
   */
  public class Scp extends SSHBase {
  
      private static final String[] FROM_ATTRS = {
          "file", "localfile", "remotefile" };
  
      private static final String[] TO_ATTRS = {
          "todir", "localtodir", "remotetodir", "localtofile", "remotetofile" };
  
      private String fromUri;
      private String toUri;
      private boolean preserveLastModified = false;
      private boolean compressed = false;
      private List<ResourceCollection> rcs = null;
      private boolean isFromRemote, isToRemote;
      private boolean isSftp = false;
      private Integer fileMode, dirMode;
  
      /**
       * Sets the file to be transferred.  This can either be a remote
       * file or a local file.  Remote files take the form:
       * <p>
       * <i>user:password@host:/directory/path/file.example</i>
       * </p>
       * Files to transfer can also include a wildcard to include all
       * files in a remote directory.  For example:
       * <p>
       * <i>user:password@host:/directory/path/*</i>
       * </p>
       *
       * @param aFromUri a string representing the file to transfer.
       */
      public void setFile(final String aFromUri) {
          setFromUri(aFromUri);
          this.isFromRemote = isRemoteUri(this.fromUri);
      }
  
      /**
       * Sets the location where files will be transferred to.
       * This can either be a remote directory or a local directory.
       * Remote directories take the form of:
       * <p>
       * <i>user:password@host:/directory/path/</i>
       * </p>
       * This parameter is required.
  
       * @param aToUri a string representing the target of the copy.
       */
      public void setTodir(final String aToUri) {
          setToUri(aToUri);
          this.isToRemote = isRemoteUri(this.toUri);
      }
  
      /**
       * Similar to {@link #setFile setFile} but explicitly states that
       * the file is a local file.  This is the only way to specify a
       * local file with a @ character.
       * @param aFromUri a string representing the source of the copy.
       * @since Ant 1.6.2
       */
      public void setLocalFile(final String aFromUri) {
          setFromUri(aFromUri);
          this.isFromRemote = false;
      }
  
      /**
       * Similar to {@link #setFile setFile} but explicitly states that
       * the file is a remote file.
       * @param aFromUri a string representing the source of the copy.
       * @since Ant 1.6.2
       */
      public void setRemoteFile(final String aFromUri) {
          validateRemoteUri("remoteFile", aFromUri);
          setFromUri(aFromUri);
          this.isFromRemote = true;
       }
  
      /**
       * Sets flag to determine if compression should
       * be used for the copy.
       * @param compressed boolean
       * @since Ant 1.9.8
       */
      public void setCompressed(boolean compressed) {
          this.compressed = compressed;
      }
  
      /**
       * Similar to {@link #setTodir setTodir} but explicitly states
       * that the directory is a local.  This is the only way to specify
       * a local directory with a @ character.
       * @param aToUri a string representing the target of the copy.
       * @since Ant 1.6.2
       */
      public void setLocalTodir(final String aToUri) {
          setToUri(aToUri);
          this.isToRemote = false;
      }
  
      /**
       * Sets flag to determine if file timestamp
       * is to be preserved during copy.
       * @param yesOrNo boolean
       * @since Ant 1.8.0
       */
      public void setPreservelastmodified(final boolean yesOrNo) {
          this.preserveLastModified = yesOrNo;
      }
  
      /**
       * Similar to {@link #setTodir setTodir} but explicitly states
       * that the directory is a remote.
       * @param aToUri a string representing the target of the copy.
       * @since Ant 1.6.2
       */
      public void setRemoteTodir(final String aToUri) {
          validateRemoteUri("remoteToDir", aToUri);
          setToUri(aToUri);
          this.isToRemote = true;
      }
  
      private static void validateRemoteUri(final String type, final String aToUri) {
          if (!isRemoteUri(aToUri)) {
              throw new BuildException(
                  "%s '%s' is invalid. The 'remoteToDir' attribute must have syntax like the following: user:password@host:/path - the :password part is optional",
                  type, aToUri);
          }
      }
  
      /**
       * Changes the file name to the given name while receiving it,
       * only useful if receiving a single file.
       * @param aToUri a string representing the target of the copy.
       * @since Ant 1.6.2
       */
      public void setLocalTofile(final String aToUri) {
          setToUri(aToUri);
          this.isToRemote = false;
      }
  
      /**
       * Changes the file name to the given name while sending it,
       * only useful if sending a single file.
       * @param aToUri a string representing the target of the copy.
       * @since Ant 1.6.2
       */
      public void setRemoteTofile(final String aToUri) {
          validateRemoteUri("remoteToFile", aToUri);
          setToUri(aToUri);
          this.isToRemote = true;
      }
  
      /**
       * Setting this to true to use sftp protocol.
       *
       * @param yesOrNo if true sftp protocol will be used.
       */
      public void setSftp(final boolean yesOrNo) {
          isSftp = yesOrNo;
      }
  
      /**
       * Set the file mode, defaults to "644".
       * @param fileMode String
       * @since Ant 1.9.5
       */
      public void setFileMode(String fileMode) {
          this.fileMode = Integer.parseInt(fileMode, 8);
      }
  
      /**
       * Set the dir mode, defaults to "755".
       * @param dirMode String
       * @since Ant 1.9.5
       */
      public void setDirMode(String dirMode) {
          this.dirMode = Integer.parseInt(dirMode, 8);
      }
  
      /**
       * Adds a FileSet transfer to remote host.  NOTE: Either
       * addFileSet() or setFile() are required.  But, not both.
       *
       * @param set FileSet to send to remote host.
       */
      public void addFileset(FileSet set) {
          add(set);
      }
  
      /**
       * Adds a ResourceCollection of local files to transfer to remote host.
       * @param res ResourceCollection to send to remote host.
       * @since Ant 1.9.7
       */
      public void add(ResourceCollection res) {
          if (rcs == null) {
              rcs = new LinkedList<>();
          }
          rcs.add(res);
      }
  
      /**
       * Initialize this task.
       * @throws BuildException on error
       */
      @Override
      public void init() throws BuildException {
          super.init();
          this.toUri = null;
          this.fromUri = null;
          this.rcs = null;
      }
  
      /**
       * Execute this task.
       * @throws BuildException on error
       */
      @Override
      public void execute() throws BuildException {
          if (toUri == null) {
              throw exactlyOne(TO_ATTRS);
          }
          if (fromUri == null && rcs == null) {
              throw exactlyOne(FROM_ATTRS, "one or more nested filesets");
          }
          try {
              if (isFromRemote && !isToRemote) {
                  download(fromUri, toUri);
              } else if (!isFromRemote && isToRemote) {
                  if (rcs != null) {
                      upload(rcs, toUri);
                  } else {
                      upload(fromUri, toUri);
                  }
              } else if (isFromRemote && isToRemote) { //NOSONAR
                  throw new BuildException(
                      "Copying from a remote server to a remote server is not supported.");
              } else {
                  throw new BuildException(
                      "'todir' and 'file' attributes must have syntax like the following: user:password@host:/path");
              }
          } catch (final Exception e) {
              if (getFailonerror()) {
                  if (e instanceof BuildException) {
                      final BuildException be = (BuildException) e;
                      if (be.getLocation() == null) {
                          be.setLocation(getLocation());
                      }
                      throw be;
                  }
                  throw new BuildException(e);
              }
              log("Caught exception: " + e.getMessage(), Project.MSG_ERR);
          }
      }
  
      private void download(final String fromSshUri, final String toPath)
          throws JSchException, IOException {
          final String file = parseUri(fromSshUri);
  
          Session session = null;
          try {
              session = openSession();
              ScpFromMessage message = null;
              if (!isSftp) {
                  message =
                      new ScpFromMessage(getVerbose(), session, file,
                                         getProject().resolveFile(toPath),
                                         fromSshUri.endsWith("*"),
                                         preserveLastModified,
                                         compressed);
              } else {
                  message =
                      new ScpFromMessageBySftp(getVerbose(), session, file,
                                               getProject().resolveFile(toPath),
                                               fromSshUri.endsWith("*"),
                                               preserveLastModified);
              }
              log("Receiving file: " + file);
              message.setLogListener(this);
              message.execute();
          } finally {
              if (session != null) {
                  session.disconnect();
              }
          }
      }
  
      private void upload(final List<ResourceCollection> rcs, final String toSshUri)
          throws IOException, JSchException {
          final String file = parseUri(toSshUri);
  
          Session session = null;
          try {
              final List<Directory> list = new ArrayList<>(rcs.size());
              for (ResourceCollection rc : rcs) {
                  if (rc instanceof FileSet && rc.isFilesystemOnly()) {
                      FileSet fs = (FileSet) rc;
                      final Directory d = createDirectory(fs);
                      if (d != null) {
                          list.add(d);
                      }
                  } else {
                      List<Directory> ds = createDirectoryCollection(rc);
                      if (ds != null) {
                          list.addAll(ds);
                      }
                  }
              }
              if (!list.isEmpty()) {
                  session = openSession();
                  ScpToMessage message;
                  if (!isSftp) {
                      message = new ScpToMessage(getVerbose(), compressed, session,
                                                 list, file, preserveLastModified);
                  } else {
                      message = new ScpToMessageBySftp(getVerbose(), session,
                                                       list, file, preserveLastModified);
                  }
                  message.setLogListener(this);
                  if (fileMode != null) {
                      message.setFileMode(fileMode);
                  }
                  if (dirMode != null) {
                      message.setDirMode(dirMode);
                  }
                  message.execute();
              }
          } finally {
              if (session != null) {
                  session.disconnect();
              }
          }
      }
  
      private void upload(final String fromPath, final String toSshUri)
          throws IOException, JSchException {
          final String file = parseUri(toSshUri);
  
          Session session = null;
          try {
              session = openSession();
              ScpToMessage message = null;
              if (!isSftp) {
                  message =
                      new ScpToMessage(getVerbose(), compressed, session,
                                       getProject().resolveFile(fromPath), file,
                                       preserveLastModified);
              } else {
                  message =
                      new ScpToMessageBySftp(getVerbose(), session,
                                             getProject().resolveFile(fromPath),
                                             file, preserveLastModified);
              }
              message.setLogListener(this);
              if (fileMode != null) {
                  message.setFileMode(fileMode);
              }
              if (dirMode != null) {
                  message.setDirMode(dirMode);
              }
              message.execute();
          } finally {
              if (session != null) {
                  session.disconnect();
              }
          }
      }
  
      private String parseUri(final String uri) {
  
          int indexOfAt = uri.indexOf('@');
          final int indexOfColon = uri.indexOf(':');
  
          if (indexOfColon > -1 && indexOfColon < indexOfAt) {
              // user:password@host:/path notation
              // everything upto the last @ before the last : is considered
              // password. (so if the path contains an @ and a : it will not work)
              int indexOfCurrentAt = indexOfAt;
              final int indexOfLastColon = uri.lastIndexOf(':');
              while (indexOfCurrentAt > -1 && indexOfCurrentAt < indexOfLastColon) {
                  indexOfAt = indexOfCurrentAt;
                  indexOfCurrentAt = uri.indexOf('@', indexOfCurrentAt + 1);
              }
              setUsername(uri.substring(0, indexOfColon));
              setPassword(uri.substring(indexOfColon + 1, indexOfAt));
          } else if (indexOfAt > -1) {
              // no password, will require keyfile
              setUsername(uri.substring(0, indexOfAt));
          } else {
              throw new BuildException("no username was given.  Can't authenticate.");
          }
  
          // For IPv6 addresses, the host part may contain more than one ":",
          // look for the last occurrence
          final int indexOfPath = uri.lastIndexOf(':');
          if (indexOfPath <= indexOfAt) {
              throw new BuildException("no remote path in %s", uri);
          }
  
          setHost(uri.substring(indexOfAt + 1, indexOfPath));
          
          loadSshConfig();
          
          if (getUserInfo().getPassword() == null
              && getUserInfo().getKeyfile() == null) {
              throw new BuildException(
                  "neither password nor keyfile for user %s has been given.  Can't authenticate.",
                  getUserInfo().getName());
          }
          
          String remotePath = uri.substring(indexOfPath + 1);
          if (remotePath.isEmpty()) {
              remotePath = ".";
          }
          return remotePath;
      }
  
      private static boolean isRemoteUri(final String uri) {
          return uri.contains("@");
      }
  
      private Directory createDirectory(final FileSet set) {
          final DirectoryScanner scanner = set.getDirectoryScanner(getProject());
          final String[] files = scanner.getIncludedFiles();
          if (files.length == 0) {
              // skip
              return null;
          }
          Directory root = new Directory(scanner.getBasedir());
          Stream.of(files).map(Directory::getPath).forEach(path -> {
              Directory current = root;
              File currentParent = scanner.getBasedir();
              for (String element : path) {
                  final File file = new File(currentParent, element);
                  if (file.isDirectory()) {
                      current.addDirectory(new Directory(file));
                      current = current.getChild(file);
                      currentParent = current.getDirectory();
                  } else if (file.isFile()) {
                      current.addFile(file);
                  }
              }
          });
          return root;
      }
  
      private List<Directory> createDirectoryCollection(final ResourceCollection rc) {
          // not a fileset or contains non-file resources
          if (!rc.isFilesystemOnly()) {
              throw new BuildException("Only FileSystem resources are supported.");
          }
  
          List<Directory> ds = new ArrayList<>();
          for (Resource r : rc) {
              if (!r.isExists()) {
                  throw new BuildException("Could not find resource %s to scp.",
                          r.toLongString());
              }
  
              FileProvider fp = r.as(FileProvider.class);
              if (fp == null) {
                  throw new BuildException("Resource %s is not a file.",
                          r.toLongString());
              }
  
              FileResource fr = ResourceUtils.asFileResource(fp);
              File baseDir = fr.getBaseDir();
              if (baseDir == null) {
                  throw new BuildException("basedir for resource %s is undefined.",
                          r.toLongString());
              }
  
              // if the basedir is set, the name will be relative to that
              String name = r.getName();
              Directory root = new Directory(baseDir);
              Directory current = root;
              File currentParent = baseDir;
              for (String element : Directory.getPath(name)) {
                  final File file = new File(currentParent, element);
                  if (file.isDirectory()) {
                      current.addDirectory(new Directory(file));
                      current = current.getChild(file);
                      currentParent = current.getDirectory();
                  } else if (file.isFile()) {
                      current.addFile(file);
                  }
              }
              ds.add(root);
          }
          return ds;
      }
  
      private void setFromUri(final String fromUri) {
          if (this.fromUri != null) {
              throw exactlyOne(FROM_ATTRS);
          }
          this.fromUri = fromUri;
      }
  
      private void setToUri(final String toUri) {
          if (this.toUri != null) {
              throw exactlyOne(TO_ATTRS);
          }
          this.toUri = toUri;
      }
  
      private BuildException exactlyOne(final String[] attrs) {
          return exactlyOne(attrs, null);
      }
  
      private BuildException exactlyOne(final String[] attrs, final String alt) {
          return new BuildException("Exactly one of [%s]%s is required",
                  String.join("|", attrs),
              alt == null ? "" : " or " + alt);
      }
  }
  