James Moger
2014-06-09 3cbd493b55f6b02df0a5efd1f714a077a8efc608
commit | author | age
af816d 1 /*
DO 2  * Copyright 2014 gitblit.com.
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *     http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 package com.gitblit.transport.ssh;
17
18 import java.io.File;
c910be 19 import java.io.FileOutputStream;
af816d 20 import java.io.IOException;
c910be 21 import java.io.OutputStreamWriter;
af816d 22 import java.net.InetSocketAddress;
c910be 23 import java.security.KeyPair;
JM 24 import java.security.KeyPairGenerator;
af816d 25 import java.text.MessageFormat;
DO 26 import java.util.concurrent.atomic.AtomicBoolean;
27
28 import org.apache.sshd.SshServer;
bf4fc5 29 import org.apache.sshd.common.io.IoServiceFactoryFactory;
DO 30 import org.apache.sshd.common.io.mina.MinaServiceFactoryFactory;
31 import org.apache.sshd.common.io.nio2.Nio2ServiceFactoryFactory;
c910be 32 import org.apache.sshd.common.keyprovider.FileKeyPairProvider;
JM 33 import org.apache.sshd.common.util.SecurityUtils;
34 import org.bouncycastle.openssl.PEMWriter;
af816d 35 import org.eclipse.jgit.internal.JGitText;
DO 36 import org.slf4j.Logger;
37 import org.slf4j.LoggerFactory;
38
e725e1 39 import com.gitblit.Constants;
af816d 40 import com.gitblit.IStoredSettings;
DO 41 import com.gitblit.Keys;
42 import com.gitblit.manager.IGitblit;
261ddf 43 import com.gitblit.transport.ssh.commands.SshCommandFactory;
c910be 44 import com.gitblit.utils.JnaUtils;
af816d 45 import com.gitblit.utils.StringUtils;
5bb55f 46 import com.gitblit.utils.WorkQueue;
c910be 47 import com.google.common.io.Files;
af816d 48
DO 49 /**
50  * Manager for the ssh transport. Roughly analogous to the
31f477 51  * {@link com.gitblit.transport.git.GitDaemon} class.
924c9b 52  *
af816d 53  */
924c9b 54 public class SshDaemon {
af816d 55
DO 56     private final Logger log = LoggerFactory.getLogger(SshDaemon.class);
57
bf4fc5 58     public static enum SshSessionBackend {
DO 59         MINA, NIO2
60     }
8982e6 61
af816d 62     /**
DO 63      * 22: IANA assigned port number for ssh. Note that this is a distinct
64      * concept from gitblit's default conf for ssh port -- this "default" is
65      * what the git protocol itself defaults to if it sees and ssh url without a
66      * port.
67      */
68     public static final int DEFAULT_PORT = 22;
69
924c9b 70     private final AtomicBoolean run;
af816d 71
924c9b 72     private final IGitblit gitblit;
JM 73     private final SshServer sshd;
af816d 74
DO 75     /**
76      * Construct the Gitblit SSH daemon.
924c9b 77      *
af816d 78      * @param gitblit
5bb55f 79      * @param workQueue
af816d 80      */
5bb55f 81     public SshDaemon(IGitblit gitblit, WorkQueue workQueue) {
af816d 82         this.gitblit = gitblit;
8982e6 83
af816d 84         IStoredSettings settings = gitblit.getSettings();
bf4fc5 85
c910be 86         // Ensure that Bouncy Castle is our JCE provider
JM 87         SecurityUtils.setRegisterBouncyCastle(true);
88
89         // Generate host RSA and DSA keypairs and create the host keypair provider
90         File rsaKeyStore = new File(gitblit.getBaseFolder(), "ssh-rsa-hostkey.pem");
91         File dsaKeyStore = new File(gitblit.getBaseFolder(), "ssh-dsa-hostkey.pem");
92         generateKeyPair(rsaKeyStore, "RSA", 2048);
93         generateKeyPair(dsaKeyStore, "DSA", 0);
94         FileKeyPairProvider hostKeyPairProvider = new FileKeyPairProvider();
95         hostKeyPairProvider.setFiles(new String [] { rsaKeyStore.getPath(), dsaKeyStore.getPath(), dsaKeyStore.getPath() });
96
97         // Client public key authenticator
98         CachingPublicKeyAuthenticator keyAuthenticator =
99                 new CachingPublicKeyAuthenticator(gitblit.getPublicKeyManager(), gitblit);
100
101         // Configure the preferred SSHD backend
bf4fc5 102         String sshBackendStr = settings.getString(Keys.git.sshBackend,
DO 103                 SshSessionBackend.NIO2.name());
104         SshSessionBackend backend = SshSessionBackend.valueOf(sshBackendStr);
105         System.setProperty(IoServiceFactoryFactory.class.getName(),
106             backend == SshSessionBackend.MINA
107                 ? MinaServiceFactoryFactory.class.getName()
108                 : Nio2ServiceFactoryFactory.class.getName());
8982e6 109
c910be 110         // Create the socket address for binding the SSH server
JM 111         int port = settings.getInteger(Keys.git.sshPort, 0);
112         String bindInterface = settings.getString(Keys.git.sshBindInterface, "");
924c9b 113         InetSocketAddress addr;
af816d 114         if (StringUtils.isEmpty(bindInterface)) {
924c9b 115             addr = new InetSocketAddress(port);
af816d 116         } else {
924c9b 117             addr = new InetSocketAddress(bindInterface, port);
af816d 118         }
DO 119
c910be 120         // Create the SSH server
924c9b 121         sshd = SshServer.setUpDefaultServer();
JM 122         sshd.setPort(addr.getPort());
123         sshd.setHost(addr.getHostName());
c910be 124         sshd.setKeyPairProvider(hostKeyPairProvider);
59e621 125         sshd.setPublickeyAuthenticator(keyAuthenticator);
448145 126         sshd.setPasswordAuthenticator(new UsernamePasswordAuthenticator(gitblit));
61b32f 127         sshd.setSessionFactory(new SshServerSessionFactory());
924c9b 128         sshd.setFileSystemFactory(new DisabledFilesystemFactory());
9ba6bc 129         sshd.setTcpipForwardingFilter(new NonForwardingFilter());
5bb55f 130         sshd.setCommandFactory(new SshCommandFactory(gitblit, workQueue));
e725e1 131         sshd.setShellFactory(new WelcomeShell(settings));
JM 132
c910be 133         // Set the server id.  This can be queried with:
JM 134         //   ssh-keyscan -t rsa,dsa -p 29418 localhost
135         String version = String.format("%s (%s-%s)", Constants.getGitBlitVersion().replace(' ', '_'),
136                 sshd.getVersion(), sshBackendStr);
e725e1 137         sshd.getProperties().put(SshServer.SERVER_IDENTIFICATION, version);
af816d 138
DO 139         run = new AtomicBoolean(false);
140     }
141
142     public String formatUrl(String gituser, String servername, String repository) {
924c9b 143         if (sshd.getPort() == DEFAULT_PORT) {
af816d 144             // standard port
1ac6d1 145             return MessageFormat.format("ssh://{0}@{1}/{2}", gituser, servername,
af816d 146                     repository);
DO 147         } else {
148             // non-standard port
149             return MessageFormat.format("ssh://{0}@{1}:{2,number,0}/{3}",
924c9b 150                     gituser, servername, sshd.getPort(), repository);
af816d 151         }
DO 152     }
153
154     /**
155      * Start this daemon on a background thread.
924c9b 156      *
af816d 157      * @throws IOException
DO 158      *             the server socket could not be opened.
159      * @throws IllegalStateException
160      *             the daemon is already running.
161      */
162     public synchronized void start() throws IOException {
163         if (run.get()) {
164             throw new IllegalStateException(JGitText.get().daemonAlreadyRunning);
165         }
166
924c9b 167         sshd.start();
af816d 168         run.set(true);
DO 169
2b5484 170         String sshBackendStr = gitblit.getSettings().getString(Keys.git.sshBackend,
JM 171                 SshSessionBackend.NIO2.name());
172
af816d 173         log.info(MessageFormat.format(
2b5484 174                 "SSH Daemon ({0}) is listening on {1}:{2,number,0}",
JM 175                 sshBackendStr, sshd.getHost(), sshd.getPort()));
af816d 176     }
DO 177
178     /** @return true if this daemon is receiving connections. */
179     public boolean isRunning() {
180         return run.get();
181     }
182
183     /** Stop this daemon. */
184     public synchronized void stop() {
185         if (run.get()) {
186             log.info("SSH Daemon stopping...");
187             run.set(false);
188
189             try {
23c416 190                 ((SshCommandFactory) sshd.getCommandFactory()).stop();
924c9b 191                 sshd.stop();
af816d 192             } catch (InterruptedException e) {
DO 193                 log.error("SSH Daemon stop interrupted", e);
194             }
b53611 195         }
JM 196     }
c910be 197
JM 198     private void generateKeyPair(File file, String algorithm, int keySize) {
199         if (file.exists()) {
200             return;
201         }
202         try {
203             KeyPairGenerator generator = SecurityUtils.getKeyPairGenerator(algorithm);
204             if (keySize != 0) {
205                 generator.initialize(keySize);
206                 log.info("Generating {}-{} SSH host keypair...", algorithm, keySize);
207             } else {
208                 log.info("Generating {} SSH host keypair...", algorithm);
209             }
210             KeyPair kp = generator.generateKeyPair();
211
212             // create an empty file and set the permissions
213             Files.touch(file);
214             try {
215                 JnaUtils.setFilemode(file, JnaUtils.S_IRUSR | JnaUtils.S_IWUSR);
01ff0f 216             } catch (UnsatisfiedLinkError | UnsupportedOperationException e) {
JM 217                 // Unexpected/Unsupported OS or Architecture
c910be 218             }
JM 219
220             FileOutputStream os = new FileOutputStream(file);
221             PEMWriter w = new PEMWriter(new OutputStreamWriter(os));
222             w.writeObject(kp);
223             w.flush();
224             w.close();
225         } catch (Exception e) {
226             log.warn(MessageFormat.format("Unable to generate {0} keypair", algorithm), e);
227             return;
228         }
229     }
af816d 230 }