All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.sshtools.commands.SshCommand Maven / Gradle / Ivy

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");
	}

}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy