/* * Copyright (C) 2005-2012 NAUMEN. All rights reserved. * * This file may be distributed and/or modified under the terms of the * GNU General Public License version 2 as published by the Free Software * Foundation and appearing in the file LICENSE.GPL included in the * packaging of this file. * */ package ru.naumen.servacc; import com.mindbright.jca.security.KeyPair; import com.mindbright.jca.security.SecureRandom; import com.mindbright.ssh2.SSH2AuthKbdInteract; import com.mindbright.ssh2.SSH2AuthPassword; import com.mindbright.ssh2.SSH2AuthPublicKey; import com.mindbright.ssh2.SSH2Authenticator; import com.mindbright.ssh2.SSH2SessionChannel; import com.mindbright.ssh2.SSH2Signature; import com.mindbright.ssh2.SSH2SimpleClient; import com.mindbright.ssh2.SSH2Transport; import com.mindbright.util.RandomSeed; import com.mindbright.util.SecureRandomAndPad; import java.io.IOException; import java.net.InetSocketAddress; import java.net.Socket; import java.net.URL; import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.concurrent.ExecutorService; import java.util.concurrent.Future; import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import ru.naumen.servacc.activechannel.ActiveChannelsRegistry; import ru.naumen.servacc.activechannel.FTPActiveChannel; import ru.naumen.servacc.activechannel.SSHActiveChannel; import ru.naumen.servacc.activechannel.SSHLocalForwardActiveChannel; import ru.naumen.servacc.activechannel.TerminalActiveChannel; import ru.naumen.servacc.activechannel.i.IActiveChannel; import ru.naumen.servacc.activechannel.i.IActiveChannelThrough; import ru.naumen.servacc.activechannel.sockets.ServerSocketWrapper; import ru.naumen.servacc.activechannel.visitors.CloseActiveChannelVisitor; import ru.naumen.servacc.backend.DualChannel; import ru.naumen.servacc.backend.mindterm.MindTermChannel; import ru.naumen.servacc.config2.Account; import ru.naumen.servacc.config2.HTTPAccount; import ru.naumen.servacc.config2.Path; import ru.naumen.servacc.config2.SSHAccount; import ru.naumen.servacc.config2.SSHKey; import ru.naumen.servacc.config2.i.IConfig; import ru.naumen.servacc.exception.ServerAccessException; import ru.naumen.servacc.platform.Command; import ru.naumen.servacc.platform.OS; import ru.naumen.servacc.telnet.ConsoleManager; import ru.naumen.servacc.util.Util; /** * @author tosha */ public class MindtermBackend implements Backend { private static final int SSH_DEFAULT_PORT = 22; private static final Logger LOGGER = LoggerFactory.getLogger(MindtermBackend.class); private static RandomSeed seed; private static SecureRandomAndPad secureRandom; private final Command browser; private final Command terminal; private final Command ftpBrowser; private final ExecutorService executor; private final ConnectionsManager connections; private SSHAccount globalThrough; private GlobalThroughView globalThroughView; private final ActiveChannelsRegistry acRegistry; private final SSHKeyLoader keyLoader; public MindtermBackend(OS system, ExecutorService executorService, ActiveChannelsRegistry acRegistry, SSHKeyLoader keyLoader) { this.browser = system.getBrowser(); this.ftpBrowser = system.getFTPBrowser(); this.terminal = system.getTerminal(); this.executor = executorService; connections = new ConnectionsManager(); this.acRegistry = acRegistry; this.keyLoader = keyLoader; } @Override public void openSSHAccount(final SSHAccount account, final String path) throws Exception { SSH2SimpleClient client; if (isConnected(account)) { client = getSSH2Client(account); // this is to force timeout when reusing a cached connection // in order to detect if a connection is hung more quickly try { final SSH2SimpleClient clientCopy = client; Future<Object> f = this.executor.submit(() -> { openSSHAccount(account, clientCopy, path); return null; }); f.get(SocketUtils.WARM_TIMEOUT, TimeUnit.MILLISECONDS); return; } catch (TimeoutException e) { removeSSHActiveChannel(account); removeConnection(account); LOGGER.error("Connection is broken, retrying", e); } } // try with "cold" timeout client = getSSH2Client(account); openSSHAccount(account, client, path); } @Override public void openHTTPAccount(HTTPAccount account) throws Exception { browser.open(buildUrl(account)); } @Override public void localPortForward(SSHAccount account, String localHost, int localPort, String remoteHost, int remotePort) throws Exception { SSH2SimpleClient client = getSSH2Client(account); client.getConnection().newLocalForward(localHost, localPort, remoteHost, remotePort); createSSHLocalForwardActiveChannel(account, localPort); } @Override public void browseViaFTP(SSHAccount account) throws Exception { SSH2SimpleClient client = getSSH2Client(account); Socket socket = openFTPBrowser(account); new FTP2SFTPProxy( client.getConnection(), socket.getInputStream(), socket.getOutputStream(), "FTP Server"); } /** * Use ssh chain to tunnel HTTP traffic * TODO use better synchronized access * * @param host * @param port * @param account */ @Override public synchronized DualChannel openProxyConnection(String host, int port, SSHAccount account) throws Exception { if (account != null) { SSH2SimpleClient client = getSSH2Client(account); return new MindTermChannel(client.getConnection().newLocalInternalForward(host, port)); } return null; } @Override public SSHAccount getThrough(Account account) { SSHAccount throughAccount = null; if (account.getThrough() != null) { throughAccount = (SSHAccount) account.getThrough(); } else if (globalThrough != null) { throughAccount = globalThrough; } return throughAccount; } @Override public void cleanup() { connections.cleanup(); } @Override public void setGlobalThrough(SSHAccount account) { globalThrough = account; connections.clearCache(); acRegistry.hideAllChannels(); } @Override public void setGlobalThroughView(GlobalThroughView view) { globalThroughView = view; } @Override public void selectNewGlobalThrough(String uniqueIdentity, IConfig config) { Path path = Path.find(config, uniqueIdentity); if (path.found()) { globalThroughView.setGlobalThroughWidget(path.path()); setGlobalThrough(path.account()); } else { clearGlobalThrough(); } } @Override public void refresh(IConfig newConfig) { String identity = ""; if (globalThrough != null) { identity = globalThrough.getUniqueIdentity(); } selectNewGlobalThrough(identity, newConfig); } @Override public void clearGlobalThrough() { globalThroughView.clearGlobalThroughWidget(); setGlobalThrough(null); } private static SecureRandomAndPad nextSecure() { if (seed == null) { seed = new RandomSeed(); } if (secureRandom == null) { secureRandom = new SecureRandomAndPad(new SecureRandom(seed.getBytesBlocking(20, false))); } return secureRandom; } private void openSSHAccount(final SSHAccount account, final SSH2SimpleClient client, final String path) { final SSH2SessionChannel session = client.getConnection().newSession(); if (session == null) { throw new ServerAccessException("Failed to create SSH session"); } try { if (!session.requestPTY("xterm", 24, 80, new byte[] {12, 0, 0, 0, 0, 0})) { client.getTransport().normalDisconnect("bye bye"); removeSSHActiveChannel(account); removeConnection(account); throw new IOException("Failed to get PTY on remote side"); } final Socket term = openTerminal(account, path); final ConsoleManager console = new ConsoleManager(term, session, account.getPassword(), account.needSudoLogin()); console.negotiateProtocolOptions(); session.changeStdIn(console.getInputStream()); session.changeStdOut(console.getOutputStream()); if (!session.doShell()) { throw new IOException("Failed to start shell on remote side"); } } catch (IOException e) { LOGGER.error("Failed to open SSH account", e); session.close(); } } private Socket openTerminal(SSHAccount account, String path) throws IOException { try (ServerSocketWrapper server = new ServerSocketWrapper(SocketUtils.createListener(SocketUtils.LOCALHOST), account, TerminalActiveChannel.class, acRegistry)) { Map<String, String> params = new HashMap<>(account.getParams()); params.put("name", path); server.getServerSocket().setSoTimeout(SocketUtils.WARM_TIMEOUT); terminal.connect(server.getServerSocket().getLocalPort(), params); // FIXME: collect children and kill it on (on?) return server.accept(); } } private String buildUrl(HTTPAccount account) throws Exception { URL url = new URL(account.getURL()); // Construct URL StringBuilder targetURL = new StringBuilder(); // protocol String protocol = url.getProtocol(); targetURL.append(protocol).append("://"); // user (authentication) info String userInfo; if (account.getLogin() != null) { String password = account.getPassword(); password = password != null ? password : ""; userInfo = account.getLogin() + ":" + password; } else { userInfo = url.getUserInfo(); } if (!Util.isEmptyOrNull(userInfo)) { targetURL.append(userInfo).append('@'); } // host and port final String remoteHost = url.getHost(); int remotePort = url.getPort(); if (remotePort < 0) { switch (protocol) { case "https" : remotePort = 443; break; case "http": default: remotePort = 80; } } String targetHost; int targetPort; SSHAccount throughAccount = getThrough(account); if (throughAccount != null) { targetHost = SocketUtils.LOCALHOST; targetPort = SocketUtils.getFreePort(); localPortForward(throughAccount, SocketUtils.LOCALHOST, targetPort, remoteHost, remotePort); } else { targetHost = remoteHost; targetPort = remotePort; } targetURL.append(targetHost).append(':').append(targetPort); // path info targetURL.append(url.getPath()); // query string if (url.getQuery() != null) { targetURL.append('?').append(url.getQuery()); } return targetURL.toString(); } private Socket openFTPBrowser(SSHAccount account) throws IOException { try (ServerSocketWrapper server = new ServerSocketWrapper(SocketUtils.createListener(SocketUtils.LOCALHOST), account, FTPActiveChannel.class, acRegistry)) { server.getServerSocket().setSoTimeout(SocketUtils.COLD_TIMEOUT); ftpBrowser.connect(server.getServerSocket().getLocalPort(), Collections.<String, String>emptyMap()); return server.accept(); } } /** * Retrieve SSH2 connection described by account (follow "through chain"). * Complex function, creating 0...n SSH2 connection. * * @param account * @return * @throws Exception */ private SSH2SimpleClient getSSH2Client(SSHAccount account) throws Exception { if (isConnected(account)) { return getConnection(account); } // follow the 'through' chain List<SSHAccount> throughChain = new ArrayList<>(); SSHAccount cur = getThrough(account); while (cur != null) { if (throughChain.contains(cur)) { // circular reference break; } throughChain.add(cur); if (isConnected(cur)) { // account is found in the cache, no need to go further break; } cur = getThrough(cur); } Collections.reverse(throughChain); SSH2SimpleClient last = null; for (SSHAccount through : throughChain) { if (isConnected(through)) { last = getConnection(through); } else { last = getSSH2Client(through, last); saveConnection(through, last); } } SSH2SimpleClient client = getSSH2Client(account, last); saveConnection(account, client); return client; } /** * Retrieve SSH2 connection described by account using through connection (if not null) as tunnel. * Simple function creating exactly 1 (one) SSH2 connection. * Do not use this function - it is only extension of another getSSH2Client. * * @param account * @param through * @return * @throws Exception */ private SSH2SimpleClient getSSH2Client(SSHAccount account, SSH2SimpleClient through) throws Exception { String host = account.getHost(); int port = account.getPort() >= 0 ? account.getPort() : SSH_DEFAULT_PORT; if (through != null) { int localPort = SocketUtils.getFreePort(); //FIXME: localize newLocalForward usage in localPortForward through.getConnection().newLocalForward(SocketUtils.LOCALHOST, localPort, host, port); return createSSH2Client(SocketUtils.LOCALHOST, localPort, true, account); } return createSSH2Client(host, port, false, account); } private SSH2SimpleClient createSSH2Client(String host, Integer port, boolean through, final SSHAccount account) throws Exception { SecureRandomAndPad secureRandomAndPad = MindtermBackend.nextSecure(); Socket sock = new Socket(); sock.connect(new InetSocketAddress(host, port), SocketUtils.COLD_TIMEOUT); SSH2Transport transport = new SSH2Transport(sock, secureRandomAndPad) { @Override protected void disconnectInternal(int i, String s, String s1, boolean b) { super.disconnectInternal(i, s, s1, b); removeSSHActiveChannel(account); removeConnection(account); } }; SSH2Authenticator auth = new SSH2Authenticator(account.getLogin()); if (account.getPassword() != null) { auth.addModule(new SSH2AuthPassword(account.getPassword())); } else { final SSHKey key = account.getSecureKey(); KeyPair keyPair = keyLoader.loadKeyPair(key); SSH2Signature rsaKey = SSH2Signature.getInstance(key.protocolType); rsaKey.setPublicKey(keyPair.getPublic()); rsaKey.initSign(keyPair.getPrivate()); auth.addModule(new SSH2AuthPublicKey(rsaKey)); } auth.addModule(new SSH2AuthKbdInteract(new SSH2PasswordInteractor(account.getPassword()))); createSSHActiveChannel(account, sock.getLocalPort(), through ? port : -1); return new SSH2SimpleClient(transport, auth); } private void saveConnection(SSHAccount account, SSH2SimpleClient client) { connections.put(account.getUniqueIdentity(), client); } private SSH2SimpleClient getConnection(SSHAccount account) { return connections.get(account.getUniqueIdentity()); } private boolean isConnected(SSHAccount account) { return connections.containsKey(account.getUniqueIdentity()); } private void removeConnection(SSHAccount account) { connections.remove(account.getUniqueIdentity()); } private void createSSHActiveChannel(SSHAccount account, int port, int portThrough) { List<String> path = account.getUniquePathReversed(); if (!path.isEmpty()) { path.remove(path.size() - 1); } IActiveChannelThrough parent = acRegistry.findChannelThrough(path); new SSHActiveChannel(parent, acRegistry, account, port, portThrough, connections).save(); } private void removeSSHActiveChannel(SSHAccount account) { IActiveChannel channel = acRegistry.findChannel(account.getUniquePathReversed()); if (channel != null) { channel.accept(new CloseActiveChannelVisitor()); } } private void createSSHLocalForwardActiveChannel(SSHAccount account, int port) { IActiveChannel channel = acRegistry.findChannel(account.getUniquePathReversed()); if (channel instanceof SSHActiveChannel) { new SSHLocalForwardActiveChannel((SSHActiveChannel)channel, acRegistry, port).save(); } } }