com.sshtools.commands.SshCommand Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of command-utils Show documentation
Show all versions of command-utils Show documentation
Utility classes for creating SSH enabled commands with Maverick Synergy
The newest version!
package com.sshtools.commands;
import java.io.File;
import java.io.IOException;
import java.util.Objects;
import java.util.Optional;
import java.util.concurrent.TimeUnit;
import com.sshtools.agent.client.SshAgentClient;
import com.sshtools.agent.exceptions.AgentNotAvailableException;
import com.sshtools.client.ExternalKeyAuthenticator;
import com.sshtools.client.IdentityFileAuthenticator;
import com.sshtools.client.PasswordAuthenticator;
import com.sshtools.client.PrivateKeyFileAuthenticator;
import com.sshtools.client.SshClient;
import com.sshtools.client.SshClientContext;
import com.sshtools.common.logger.Log;
import com.sshtools.common.logger.Log.Level;
import com.sshtools.common.publickey.SshPrivateKeyFileFactory;
import com.sshtools.common.ssh.SecurityLevel;
import com.sshtools.common.ssh.SshException;
import com.sshtools.common.ssh.components.jce.JCEProvider;
import com.sshtools.common.util.Utils;
import com.sshtools.jaul.Phase;
import com.sshtools.sequins.Prompter.PromptContextBuilder;
import com.sshtools.synergy.ssh.SshContext;
import picocli.CommandLine.Command;
import picocli.CommandLine.Option;
@Command
public abstract class SshCommand extends AbstractJadaptiveCommand {
static {
JCEProvider.enableBouncyCastle(true);
}
@Option(names = { "-D", "--disable-agent" }, description = "disable ssh-agent authentication")
boolean disableAgent = false;
@Option(names = { "-I", "--ignore-identities" }, description = "ignore default identities in .ssh folder")
boolean ignoreIdentities = false;
@Option(names = { "-a",
"--agent" }, paramLabel = "SOCKET", description = "the path to the ssh-agent socket or named pipe")
String agentSocket;
@Option(names = { "-i",
"--identity" }, paramLabel = "FILE", description = "the identity file you want to authenticate with")
File identityFile;
@Option(names = { "-l", "--log" }, paramLabel = "LEVEL", description = "Log to console (INFO,DEBUG)")
Optional maverickConsoleLevel;
@Option(names = { "-c", "--cipher" }, paramLabel = "CIPHER", description = "Set the preferred cipher (C->S & S->C)")
Optional cipher;
@Option(names = { "-z", "--compression" }, paramLabel = "TYPE", description = "Set the preferred compression (C->S & S->C)")
Optional compression;
@Option(names = { "-s", "--security-level" }, paramLabel = "LEVEL", description = "Set the security level")
Optional securityLevel;
@Option(names = { "-k", "--kex" }, paramLabel = "KEX", description = "Set the preferred key exchange")
Optional kex;
@Option(names = { "-m", "--mac" }, paramLabel = "MAC", description = "Set the preferred MAC")
Optional mac;
@Option(names = { "-x", "--public-key-algo" }, paramLabel = "ALGO", description = "Set the preferred public key algorithm")
Optional publicKeyAlgortithm;
long authenticationTimeout = 120;
private char[] cachedPassword;
private char[] cachedPassphrase;
protected SshClient ssh;
protected SshCommand(Optional defaultPhase) {
super(defaultPhase);
}
@Override
protected final Integer onCall() throws Exception {
initCommand();
int exitCode = runCommand();
afterCommand();
return exitCode;
}
public void initCommand() throws Exception {
if(this.ssh == null) {
maverickConsoleLevel.ifPresent(l -> Log.enableConsole(Level.DEBUG));
beforeCommand();
getTerminal().messageln("Connecting to {0}@{1}:{2,number,#}", getUsername(), getHost(), getPort());
this.ssh = connect();
}
}
protected abstract int runCommand();
protected void beforeCommand() {
}
protected void afterCommand() throws IOException, SshException {
}
protected abstract String getCommandName();
public SshClient connect() throws IOException, SshException {
return connect(false, disableAgent, ignoreIdentities, identityFile, true);
}
public SshClient connect(boolean ignorePassword, boolean verbose) throws IOException, SshException {
return connect(ignorePassword, disableAgent, ignoreIdentities, identityFile, verbose);
}
public SshClient connect(boolean ignorePassword, boolean disableAgent, boolean ignoreIdentities, File identityFile,
boolean verbose) throws IOException, SshException {
var sshContext = new SshClientContext(securityLevel.orElse(SecurityLevel.STRONG));
if(securityLevel.isPresent()) {
if(!cipher.isEmpty()) {
sshContext.setPreferredCipherCS(cipher.get());
sshContext.setPreferredCipherSC(cipher.get());
}
} else {
if(cipher.isEmpty()) {
sshContext.setPreferredCipherCS(SshContext.CIPHER_AES256_CTR);
sshContext.setPreferredCipherSC(SshContext.CIPHER_AES256_CTR);
}
else {
sshContext.setPreferredCipherCS(cipher.get());
sshContext.setPreferredCipherSC(cipher.get());
}
}
sshContext.setPreferredCompressionCS(compression.orElse("none"));
sshContext.setPreferredCompressionSC(compression.orElse("none"));
if(kex.isPresent())
sshContext.setPreferredKeyExchange(kex.get());
if(mac.isPresent()) {
sshContext.setPreferredMacCS(mac.get());
sshContext.setPreferredMacSC(mac.get());
}
if(publicKeyAlgortithm.isPresent()) {
sshContext.setPreferredPublicKey(publicKeyAlgortithm.get());
}
var ssh = new SshClient(getHost(), getPort(), getUsername(), sshContext);
if (!disableAgent) {
try {
var customAgentSocket = !Utils.isBlank(agentSocket);
if (!customAgentSocket) {
agentSocket = SshAgentClient.getEnvironmentSocket();
}
try (var agent = SshAgentClient.connectOpenSSHAgent(getCommandName(), agentSocket)) {
if (ssh.authenticate(new ExternalKeyAuthenticator(agent),
TimeUnit.SECONDS.toMillis(authenticationTimeout))) {
if (verbose)
getTerminal().messageln("Authenticated by sshagent");
}
} catch (AgentNotAvailableException | IOException e) {
if (customAgentSocket) {
if (verbose)
getTerminal().error(isVerboseExceptions(), "Failed to connect to ssh-agent", e);
}
}
} catch (AgentNotAvailableException e) {
if (verbose)
getTerminal().errorln("No agent is available");
}
}
if (!ignoreIdentities && !ssh.isAuthenticated()) {
var buf = new StringBuffer();
if (ssh.authenticate(new IdentityFileAuthenticator(
(keyinfo) -> {
if (verbose)
getTerminal().messageln("Found acceptable key {0}", keyinfo);
if (cachedPassphrase != null) {
return new String(cachedPassphrase);
}
var tmp = readPassword("Passphrase");
buf.setLength(0);
buf.append(tmp);
return tmp;
}), TimeUnit.SECONDS.toMillis(authenticationTimeout))) {
cachedPassphrase = buf.toString().toCharArray();
}
}
if (!ssh.isAuthenticated() && Objects.nonNull(identityFile)) {
if (!identityFile.exists()) {
if (verbose)
getTerminal().errorln("The identity file provided does not exist");
} else {
var file = SshPrivateKeyFileFactory.parse(identityFile);
String passphrase = null;
if (file.isPassphraseProtected()) {
if (cachedPassphrase != null) {
passphrase = new String(cachedPassphrase);
} else {
passphrase = readPassword("Passphrase");
}
}
if (ssh.authenticate(new PrivateKeyFileAuthenticator(identityFile, passphrase),
TimeUnit.SECONDS.toMillis(authenticationTimeout))) {
if (passphrase != null) {
cachedPassphrase = passphrase.toCharArray();
}
} else {
if (verbose)
getTerminal().errorln("The identity file provided could not authenticate");
}
}
}
if (!ignorePassword) {
if (!ssh.isAuthenticated()) {
if (ssh.getAuthenticationMethods().contains("password")
|| ssh.getAuthenticationMethods().contains("keyboard-interactive")) {
for (int i = 0; i < 3; i++) {
while (ssh.isConnected() && !ssh.isAuthenticated()) {
var buf = new StringBuffer();
if (ssh.authenticate(PasswordAuthenticator.of(() -> {
if (cachedPassword != null) {
return new String(cachedPassword);
}
var tmp = readPassword("Password");
buf.setLength(0);
buf.append(tmp);
return tmp;
}), TimeUnit.SECONDS.toMillis(authenticationTimeout))) {
if (cachedPassword == null) {
cachedPassword = buf.toString().toCharArray();
}
break;
}
}
}
} else {
if (verbose)
getTerminal().messageln("Password authentication is not supported");
}
}
}
if (!ssh.isConnected()) {
throw new IOException("The connection could not be established!");
}
if (!ssh.isAuthenticated()) {
throw new IOException("The connection could not be authenticated!");
}
onConnected(ssh);
return ssh;
}
protected void onConnected(SshClient ssh) {
}
protected String readPassword(String prompt) {
return readPassword(prompt, true);
}
protected String readPassword(String prompt, boolean useCommandPassword) {
if (useCommandPassword) {
String passwd = System.getProperty("maverick.command.password");
if (Utils.isNotBlank(passwd)) {
return passwd;
}
}
return new String(
getTerminal().password(PromptContextBuilder.builder().withUse(getUsername()).build(), prompt));
}
public String getCachedPasswordOrPrompt(String prompt) {
if (cachedPassword != null) {
return new String(cachedPassword);
} else {
var tmp = readPassword(prompt);
cachedPassword = tmp.toCharArray();
return tmp;
}
}
public abstract String getHost();
public int getPort() {
return 22;
}
public String getUsername() {
return System.getProperty("user.name");
}
protected boolean isRoot() {
return getUsername().equals("root");
}
}