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.File;
  import java.io.IOException;
  import java.util.HashMap;
  import java.util.Locale;
  import java.util.Map;
  import java.util.Properties;
  
  import org.apache.ivy.core.IvyContext;
  import org.apache.ivy.core.event.IvyEvent;
  import org.apache.ivy.core.event.IvyListener;
  import org.apache.ivy.core.event.resolve.EndResolveEvent;
  import org.apache.ivy.util.Checks;
  import org.apache.ivy.util.Credentials;
  import org.apache.ivy.util.CredentialsUtil;
  import org.apache.ivy.util.Message;
  
  import com.jcraft.jsch.ChannelSftp;
  import com.jcraft.jsch.JSch;
  import com.jcraft.jsch.JSchException;
  import com.jcraft.jsch.Session;
  import com.jcraft.jsch.UIKeyboardInteractive;
  import com.jcraft.jsch.UserInfo;
  import com.jcraft.jsch.agentproxy.Connector;
  import com.jcraft.jsch.agentproxy.ConnectorFactory;
  import com.jcraft.jsch.agentproxy.RemoteIdentityRepository;
  
  /**
   * a class to cache SSH Connections and Channel for the SSH Repository each session is defined by
   * connecting user / host / port two maps are used to find cache entries one map is using the above
   * keys, the other uses the session itself
   */
  public final class SshCache {
  
      private static final int SSH_DEFAULT_PORT = 22;
  
      private SshCache() {
      }
  
      private static SshCache instance = new SshCache();
  
      public static SshCache getInstance() {
          return instance;
      }
  
      private class Entry {
          private Session session = null;
  
          private ChannelSftp channelSftp = null;
  
          private String host = null;
  
          private String user = null;
  
          private int port = SSH_DEFAULT_PORT;
  
          /**
           * @return the host
           */
          public String getHost() {
              return host;
          }
  
          /**
           * @return the port
           */
          public int getPort() {
              return port;
          }
  
          /**
           * @return the user
           */
          public String getUser() {
              return user;
          }
  
          public Entry(Session newSession, String newUser, String newHost, int newPort) {
              session = newSession;
              host = newHost;
              user = newUser;
              port = newPort;
              IvyContext.getContext().getEventManager().addIvyListener(new IvyListener() {
                  public void progress(IvyEvent event) {
                      event.getSource().removeIvyListener(this);
                      clearSession(session);
                  }
              }, EndResolveEvent.NAME);
          }
  
          /**
           * attach an sftp channel to this cache entry
           *
           * @param newChannel
           *            to attach
           */
          public void setChannelSftp(ChannelSftp newChannel) {
              if (channelSftp != null && newChannel != null) {
                  throw new IllegalStateException("Only one sftp channelSftp per session allowed");
              }
              this.channelSftp = newChannel;
          }
  
          /**
           * @return the attached sftp channel
           */
          public ChannelSftp getChannelSftp() {
              return channelSftp;
          }
  
          /**
           * @return the session
           */
          private Session getSession() {
              return session;
          }
  
          /**
           * remove channelSftp and disconnect if necessary
           */
          public void releaseChannelSftp() {
              if (channelSftp != null) {
                  if (channelSftp.isConnected()) {
                      Message.verbose(":: SFTP :: closing sftp connection from " + host + "...");
                      channelSftp.disconnect();
                      channelSftp = null;
                      Message.verbose(":: SFTP :: sftp connection closed from " + host);
                  }
              }
          }
      }
  
      /**
       * key is username / host / port
       *
       * @see #createCacheKey(String, String, int) for details
       */
      private final Map<String, Entry> uriCacheMap = new HashMap<>();
  
      /**
       * key is the session itself
       */
      private final Map<Session, Entry> sessionCacheMap = new HashMap<>();
  
      /**
       * retrieves a session entry for a given hostname from the cache
       *
       * @param user
       *            to retrieve session for
       * @param host
       *            ditto
       * @param port
       *            ditto
       * @return null or the existing entry
       */
      private Entry getCacheEntry(String user, String host, int port) {
          return uriCacheMap.get(createCacheKey(user, host, port));
      }
  
      /**
       * Creates a combined cache key from the given key parts
       *
       * @param user
       *            name of the user
       * @param host
       *            of the connection
       * @param port
       *            of the connection
       * @return key for the cache
       */
      private static String createCacheKey(String user, String host, int port) {
          String portToUse = "22";
          if (port != -1 && port != SSH_DEFAULT_PORT) {
              portToUse = Integer.toString(port);
          }
          return user.trim().toLowerCase(Locale.US) + "@" + host.trim().toLowerCase(Locale.US) + ":"
                  + portToUse;
      }
  
      /**
       * retrieves a session entry for a given session from the cache
       *
       * @param session
       *            to retrieve cache entry for
       * @return null or the existing entry
       */
      private Entry getCacheEntry(Session session) {
          return sessionCacheMap.get(session);
      }
  
      /**
       * Sets a session to a given combined key into the cache If an old session object already
       * exists, close and remove it
       *
       * @param user
       *            of the session
       * @param host
       *            of the session
       * @param port
       *            of the session
       * @param newSession
       *            Session to save
       */
      private void setSession(String user, String host, int port, Session newSession) {
          Entry entry = uriCacheMap.get(createCacheKey(user, host, port));
          Session oldSession = null;
          if (entry != null) {
              oldSession = entry.getSession();
          }
          if (oldSession != null && !oldSession.equals(newSession) && oldSession.isConnected()) {
              entry.releaseChannelSftp();
              String oldhost = oldSession.getHost();
              Message.verbose(":: SSH :: closing ssh connection from " + oldhost + "...");
              oldSession.disconnect();
              Message.verbose(":: SSH :: ssh connection closed from " + oldhost);
          }
          if (newSession == null && entry != null) {
              uriCacheMap.remove(createCacheKey(user, host, port));
              if (entry.getSession() != null) {
                  sessionCacheMap.remove(entry.getSession());
              }
          } else {
              Entry newEntry = new Entry(newSession, user, host, port);
              uriCacheMap.put(createCacheKey(user, host, port), newEntry);
              sessionCacheMap.put(newSession, newEntry);
          }
      }
  
      /**
       * discards session entries from the cache
       *
       * @param session
       *            to clear
       */
      public void clearSession(Session session) {
          Entry entry = sessionCacheMap.get(session);
          if (entry != null) {
              setSession(entry.getUser(), entry.getHost(), entry.getPort(), null);
          }
      }
  
      /**
       * retrieves an sftp channel from the cache
       *
       * @param session
       *            to connect to
       * @return channelSftp or null if not successful (channel not existent or dead)
       * @throws IOException should never happen
       */
      public ChannelSftp getChannelSftp(Session session) throws IOException {
          ChannelSftp channel = null;
          Entry entry = getCacheEntry(session);
          if (entry != null) {
              channel = entry.getChannelSftp();
              if (channel != null && !channel.isConnected()) {
                  entry.releaseChannelSftp();
                  channel = null;
              }
          }
          return channel;
      }
  
      /**
       * attaches a channelSftp to an existing session cache entry
       *
       * @param session
       *            to attach the channel to
       * @param channel
       *            channel to attach
       */
      public void attachChannelSftp(Session session, ChannelSftp channel) {
          Entry entry = getCacheEntry(session);
          if (entry == null) {
              throw new IllegalArgumentException("No entry for " + session + " in the cache");
          }
          entry.setChannelSftp(channel);
      }
  
      /**
       * Attempts to connect to a local SSH agent (using either UNIX sockets or PuTTY's Pageant)
       *
       * @param jsch
       *            Connection to be attached to an available local agent
       * @return true if connected to agent, false otherwise
       */
      private boolean attemptAgentUse(JSch jsch) {
          try {
              Connector con = ConnectorFactory.getDefault().createConnector();
              jsch.setIdentityRepository(new RemoteIdentityRepository(con));
              return true;
          } catch (Exception e) {
              Message.verbose(":: SSH :: Failure connecting to agent :: " + e.toString());
              return false;
          }
      }
  
      /**
       * Gets a session from the cache or establishes a new session if necessary
       *
       * @param host
       *            to connect to
       * @param port
       *            to use for session (-1 == use standard port)
       * @param username
       *            for the session to use
       * @param userPassword
       *            to use for authentication (optional)
       * @param pemFile
       *            File to use for public key authentication
       * @param pemPassword
       *            to use for accessing the pemFile (optional)
       * @param passFile
       *            to store credentials
       * @param allowedAgentUse
       *            Whether to communicate with an agent for authentication
       * @return session or null if not successful
       * @throws IOException if something goes wrong
       */
      public Session getSession(String host, int port, String username, String userPassword,
              File pemFile, String pemPassword, File passFile, boolean allowedAgentUse)
              throws IOException {
          Checks.checkNotNull(host, "host");
          Checks.checkNotNull(username, "user");
          Entry entry = getCacheEntry(username, host, port);
          Session session = null;
          if (entry != null) {
              session = entry.getSession();
          }
          if (session == null || !session.isConnected()) {
              Message.verbose(":: SSH :: connecting to " + host + "...");
              try {
                  JSch jsch = new JSch();
                  if (port != -1) {
                      session = jsch.getSession(username, host, port);
                  } else {
                      session = jsch.getSession(username, host);
                  }
                  if (allowedAgentUse) {
                      attemptAgentUse(jsch);
                  }
                  if (pemFile != null) {
                      jsch.addIdentity(pemFile.getAbsolutePath(), pemPassword);
                  }
                  session.setUserInfo(new CfUserInfo(host, username, userPassword, pemFile,
                          pemPassword, passFile));
                  session.setDaemonThread(true);
  
                  Properties config = new Properties();
                  config.setProperty("PreferredAuthentications",
                      "publickey,keyboard-interactive,password");
                  session.setConfig(config);
  
                  session.connect();
                  Message.verbose(":: SSH :: connected to " + host + "!");
                  setSession(username, host, port, session);
              } catch (JSchException e) {
                  if (passFile != null && passFile.exists()) {
                      passFile.delete();
                  }
                  throw new IOException(e.getMessage(), e);
              }
          }
          return session;
      }
  
      /**
       * feeds in password silently into JSch
       */
      private static class CfUserInfo implements UserInfo, UIKeyboardInteractive {
  
          private String userPassword;
  
          private String pemPassword;
  
          private String userName;
  
          private final File pemFile;
  
          private final String host;
  
          private final File passfile;
  
          public CfUserInfo(String host, String userName, String userPassword, File pemFile,
                  String pemPassword, File passfile) {
              this.userPassword = userPassword;
              this.pemPassword = pemPassword;
              this.host = host;
              this.passfile = passfile;
              this.userName = userName;
              this.pemFile = pemFile;
          }
  
          public void showMessage(String message) {
              Message.info(message);
          }
  
          public boolean promptYesNo(String message) {
              return true;
          }
  
          public boolean promptPassword(String message) {
              return true;
          }
  
          public boolean promptPassphrase(String message) {
              return true;
          }
  
          public String getPassword() {
              if (userPassword == null) {
                  Credentials c = CredentialsUtil.promptCredentials(new Credentials(null, host,
                          userName, userPassword), passfile);
                  if (c != null) {
                      userName = c.getUserName();
                      userPassword = c.getPasswd();
                  }
              }
              return userPassword;
          }
  
          public String getPassphrase() {
              if (pemPassword == null && pemFile != null) {
                  Credentials c = CredentialsUtil.promptCredentials(
                      new Credentials(null, pemFile.getAbsolutePath(), userName, pemPassword),
                      passfile);
                  if (c != null) {
                      userName = c.getUserName();
                      pemPassword = c.getPasswd();
                  }
              }
              return pemPassword;
          }
  
          public String[] promptKeyboardInteractive(String destination, String name,
                  String instruction, String[] prompt, boolean[] echo) {
              return new String[] {getPassword()};
          }
      }
  }
  