| /* |
| * 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()}; |
| } |
| } |
| } |