/* * Copyright 2014 gitblit.com. * * Licensed 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 * * http://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 com.gitblit.transport.ssh; import java.io.File; import java.io.FileOutputStream; import java.io.IOException; import java.io.OutputStreamWriter; import java.net.InetSocketAddress; import java.security.KeyPair; import java.security.KeyPairGenerator; import java.text.MessageFormat; import java.util.ArrayList; import java.util.List; import java.util.Locale; import java.util.concurrent.atomic.AtomicBoolean; import org.apache.sshd.common.NamedFactory; import org.apache.sshd.common.io.IoServiceFactoryFactory; import org.apache.sshd.common.io.mina.MinaServiceFactoryFactory; import org.apache.sshd.common.io.nio2.Nio2ServiceFactoryFactory; import org.apache.sshd.common.util.SecurityUtils; import org.apache.sshd.server.SshServer; import org.apache.sshd.server.auth.CachingPublicKeyAuthenticator; import org.apache.sshd.server.auth.UserAuth; import org.apache.sshd.server.auth.UserAuthKeyboardInteractiveFactory; import org.apache.sshd.server.auth.UserAuthPasswordFactory; import org.apache.sshd.server.auth.UserAuthPublicKeyFactory; import org.apache.sshd.server.auth.gss.GSSAuthenticator; import org.apache.sshd.server.auth.gss.UserAuthGSSFactory; import org.bouncycastle.openssl.PEMWriter; import org.eclipse.jgit.internal.JGitText; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.gitblit.Constants; import com.gitblit.IStoredSettings; import com.gitblit.Keys; import com.gitblit.manager.IGitblit; import com.gitblit.transport.ssh.commands.SshCommandFactory; import com.gitblit.utils.JnaUtils; import com.gitblit.utils.StringUtils; import com.gitblit.utils.WorkQueue; import com.google.common.io.Files; /** * Manager for the ssh transport. Roughly analogous to the * {@link com.gitblit.transport.git.GitDaemon} class. * */ public class SshDaemon { private final Logger log = LoggerFactory.getLogger(SshDaemon.class); public static enum SshSessionBackend { MINA, NIO2 } /** * 22: IANA assigned port number for ssh. Note that this is a distinct * concept from gitblit's default conf for ssh port -- this "default" is * what the git protocol itself defaults to if it sees and ssh url without a * port. */ public static final int DEFAULT_PORT = 22; private final AtomicBoolean run; private final IGitblit gitblit; private final SshServer sshd; /** * Construct the Gitblit SSH daemon. * * @param gitblit * @param workQueue */ public SshDaemon(IGitblit gitblit, WorkQueue workQueue) { this.gitblit = gitblit; IStoredSettings settings = gitblit.getSettings(); // Ensure that Bouncy Castle is our JCE provider SecurityUtils.setRegisterBouncyCastle(true); if (SecurityUtils.isBouncyCastleRegistered()) { log.debug("BouncyCastle is registered as a JCE provider"); } // Generate host RSA and DSA keypairs and create the host keypair provider File rsaKeyStore = new File(gitblit.getBaseFolder(), "ssh-rsa-hostkey.pem"); File dsaKeyStore = new File(gitblit.getBaseFolder(), "ssh-dsa-hostkey.pem"); generateKeyPair(rsaKeyStore, "RSA", 2048); generateKeyPair(dsaKeyStore, "DSA", 0); FileKeyPairProvider hostKeyPairProvider = new FileKeyPairProvider(); hostKeyPairProvider.setFiles(new String [] { rsaKeyStore.getPath(), dsaKeyStore.getPath(), dsaKeyStore.getPath() }); // Client public key authenticator SshKeyAuthenticator keyAuthenticator = new SshKeyAuthenticator(gitblit.getPublicKeyManager(), gitblit); // Configure the preferred SSHD backend String sshBackendStr = settings.getString(Keys.git.sshBackend, SshSessionBackend.NIO2.name()); SshSessionBackend backend = SshSessionBackend.valueOf(sshBackendStr); System.setProperty(IoServiceFactoryFactory.class.getName(), backend == SshSessionBackend.MINA ? MinaServiceFactoryFactory.class.getName() : Nio2ServiceFactoryFactory.class.getName()); // Create the socket address for binding the SSH server int port = settings.getInteger(Keys.git.sshPort, 0); String bindInterface = settings.getString(Keys.git.sshBindInterface, ""); InetSocketAddress addr; if (StringUtils.isEmpty(bindInterface)) { addr = new InetSocketAddress(port); } else { addr = new InetSocketAddress(bindInterface, port); } //Will do GSS ? GSSAuthenticator gssAuthenticator = null; if(settings.getBoolean(Keys.git.sshWithKrb5, false)) { gssAuthenticator = new SshKrbAuthenticator(gitblit, settings); String keytabString = settings.getString(Keys.git.sshKrb5Keytab, ""); if(! keytabString.isEmpty()) { gssAuthenticator.setKeytabFile(keytabString); } String servicePrincipalName = settings.getString(Keys.git.sshKrb5ServicePrincipalName, ""); if(! servicePrincipalName.isEmpty()) { gssAuthenticator.setServicePrincipalName(servicePrincipalName); } } //Sort the authenticators for sshd List> userAuthFactories = new ArrayList<>(); String sshAuthenticatorsOrderString = settings.getString(Keys.git.sshAuthenticatorsOrder, "password,keyboard-interactive,publickey"); for(String authenticator: sshAuthenticatorsOrderString.split(",")) { String authenticatorName = authenticator.trim().toLowerCase(Locale.US); switch (authenticatorName) { case "gssapi-with-mic": if(gssAuthenticator != null) { userAuthFactories.add(new UserAuthGSSFactory()); } break; case "publickey": userAuthFactories.add(new UserAuthPublicKeyFactory()); break; case "password": userAuthFactories.add(new UserAuthPasswordFactory()); break; case "keyboard-interactive": userAuthFactories.add(new UserAuthKeyboardInteractiveFactory()); break; default: log.error("Unknown ssh authenticator: '{}'", authenticatorName); } } // Create the SSH server sshd = SshServer.setUpDefaultServer(); sshd.setPort(addr.getPort()); sshd.setHost(addr.getHostName()); sshd.setKeyPairProvider(hostKeyPairProvider); sshd.setPublickeyAuthenticator(new CachingPublicKeyAuthenticator(keyAuthenticator)); sshd.setPasswordAuthenticator(new UsernamePasswordAuthenticator(gitblit)); if(gssAuthenticator != null) { sshd.setGSSAuthenticator(gssAuthenticator); } sshd.setUserAuthFactories(userAuthFactories); sshd.setSessionFactory(new SshServerSessionFactory()); sshd.setFileSystemFactory(new DisabledFilesystemFactory()); sshd.setTcpipForwardingFilter(new NonForwardingFilter()); sshd.setCommandFactory(new SshCommandFactory(gitblit, workQueue)); sshd.setShellFactory(new WelcomeShell(settings)); // Set the server id. This can be queried with: // ssh-keyscan -t rsa,dsa -p 29418 localhost String version = String.format("%s (%s-%s)", Constants.getGitBlitVersion().replace(' ', '_'), sshd.getVersion(), sshBackendStr); sshd.getProperties().put(SshServer.SERVER_IDENTIFICATION, version); run = new AtomicBoolean(false); } public String formatUrl(String gituser, String servername, String repository) { IStoredSettings settings = gitblit.getSettings(); int port = sshd.getPort(); int displayPort = settings.getInteger(Keys.git.sshAdvertisedPort, port); String displayServername = settings.getString(Keys.git.sshAdvertisedHost, ""); if(displayServername.isEmpty()) { displayServername = servername; } if (displayPort == DEFAULT_PORT) { // standard port return MessageFormat.format("ssh://{0}@{1}/{2}", gituser, displayServername, repository); } else { // non-standard port return MessageFormat.format("ssh://{0}@{1}:{2,number,0}/{3}", gituser, displayServername, displayPort, repository); } } /** * Start this daemon on a background thread. * * @throws IOException * the server socket could not be opened. * @throws IllegalStateException * the daemon is already running. */ public synchronized void start() throws IOException { if (run.get()) { throw new IllegalStateException(JGitText.get().daemonAlreadyRunning); } sshd.start(); run.set(true); String sshBackendStr = gitblit.getSettings().getString(Keys.git.sshBackend, SshSessionBackend.NIO2.name()); log.info(MessageFormat.format( "SSH Daemon ({0}) is listening on {1}:{2,number,0}", sshBackendStr, sshd.getHost(), sshd.getPort())); } /** @return true if this daemon is receiving connections. */ public boolean isRunning() { return run.get(); } /** Stop this daemon. */ public synchronized void stop() { if (run.get()) { log.info("SSH Daemon stopping..."); run.set(false); try { ((SshCommandFactory) sshd.getCommandFactory()).stop(); sshd.stop(); } catch (IOException e) { log.error("SSH Daemon stop interrupted", e); } } } private void generateKeyPair(File file, String algorithm, int keySize) { if (file.exists()) { return; } try { KeyPairGenerator generator = SecurityUtils.getKeyPairGenerator(algorithm); if (keySize != 0) { generator.initialize(keySize); log.info("Generating {}-{} SSH host keypair...", algorithm, keySize); } else { log.info("Generating {} SSH host keypair...", algorithm); } KeyPair kp = generator.generateKeyPair(); // create an empty file and set the permissions Files.touch(file); try { JnaUtils.setFilemode(file, JnaUtils.S_IRUSR | JnaUtils.S_IWUSR); } catch (UnsatisfiedLinkError | UnsupportedOperationException e) { // Unexpected/Unsupported OS or Architecture } FileOutputStream os = new FileOutputStream(file); PEMWriter w = new PEMWriter(new OutputStreamWriter(os)); w.writeObject(kp); w.flush(); w.close(); } catch (Exception e) { log.warn(MessageFormat.format("Unable to generate {0} keypair", algorithm), e); return; } } }