blob: f3cbb82b1b926367a3d1c2d76051f8fd6faf98fe [file] [log] [blame]
/*
* 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()};
}
}
}