James Moger
2014-09-30 6fce727aca709a9940de0f2eac419b4791c3bf9a
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);
5604f3 88         if (SecurityUtils.isBouncyCastleRegistered()) {
JM 89             log.debug("BouncyCastle is registered as a JCE provider");
90         }
c910be 91
JM 92         // Generate host RSA and DSA keypairs and create the host keypair provider
93         File rsaKeyStore = new File(gitblit.getBaseFolder(), "ssh-rsa-hostkey.pem");
94         File dsaKeyStore = new File(gitblit.getBaseFolder(), "ssh-dsa-hostkey.pem");
95         generateKeyPair(rsaKeyStore, "RSA", 2048);
96         generateKeyPair(dsaKeyStore, "DSA", 0);
97         FileKeyPairProvider hostKeyPairProvider = new FileKeyPairProvider();
98         hostKeyPairProvider.setFiles(new String [] { rsaKeyStore.getPath(), dsaKeyStore.getPath(), dsaKeyStore.getPath() });
99
100         // Client public key authenticator
101         CachingPublicKeyAuthenticator keyAuthenticator =
102                 new CachingPublicKeyAuthenticator(gitblit.getPublicKeyManager(), gitblit);
103
104         // Configure the preferred SSHD backend
bf4fc5 105         String sshBackendStr = settings.getString(Keys.git.sshBackend,
DO 106                 SshSessionBackend.NIO2.name());
107         SshSessionBackend backend = SshSessionBackend.valueOf(sshBackendStr);
108         System.setProperty(IoServiceFactoryFactory.class.getName(),
109             backend == SshSessionBackend.MINA
110                 ? MinaServiceFactoryFactory.class.getName()
111                 : Nio2ServiceFactoryFactory.class.getName());
8982e6 112
c910be 113         // Create the socket address for binding the SSH server
JM 114         int port = settings.getInteger(Keys.git.sshPort, 0);
115         String bindInterface = settings.getString(Keys.git.sshBindInterface, "");
924c9b 116         InetSocketAddress addr;
af816d 117         if (StringUtils.isEmpty(bindInterface)) {
924c9b 118             addr = new InetSocketAddress(port);
af816d 119         } else {
924c9b 120             addr = new InetSocketAddress(bindInterface, port);
af816d 121         }
DO 122
c910be 123         // Create the SSH server
924c9b 124         sshd = SshServer.setUpDefaultServer();
JM 125         sshd.setPort(addr.getPort());
126         sshd.setHost(addr.getHostName());
c910be 127         sshd.setKeyPairProvider(hostKeyPairProvider);
59e621 128         sshd.setPublickeyAuthenticator(keyAuthenticator);
448145 129         sshd.setPasswordAuthenticator(new UsernamePasswordAuthenticator(gitblit));
61b32f 130         sshd.setSessionFactory(new SshServerSessionFactory());
924c9b 131         sshd.setFileSystemFactory(new DisabledFilesystemFactory());
9ba6bc 132         sshd.setTcpipForwardingFilter(new NonForwardingFilter());
5bb55f 133         sshd.setCommandFactory(new SshCommandFactory(gitblit, workQueue));
e725e1 134         sshd.setShellFactory(new WelcomeShell(settings));
JM 135
c910be 136         // Set the server id.  This can be queried with:
JM 137         //   ssh-keyscan -t rsa,dsa -p 29418 localhost
138         String version = String.format("%s (%s-%s)", Constants.getGitBlitVersion().replace(' ', '_'),
139                 sshd.getVersion(), sshBackendStr);
e725e1 140         sshd.getProperties().put(SshServer.SERVER_IDENTIFICATION, version);
af816d 141
DO 142         run = new AtomicBoolean(false);
143     }
144
145     public String formatUrl(String gituser, String servername, String repository) {
924c9b 146         if (sshd.getPort() == DEFAULT_PORT) {
af816d 147             // standard port
1ac6d1 148             return MessageFormat.format("ssh://{0}@{1}/{2}", gituser, servername,
af816d 149                     repository);
DO 150         } else {
151             // non-standard port
152             return MessageFormat.format("ssh://{0}@{1}:{2,number,0}/{3}",
924c9b 153                     gituser, servername, sshd.getPort(), repository);
af816d 154         }
DO 155     }
156
157     /**
158      * Start this daemon on a background thread.
924c9b 159      *
af816d 160      * @throws IOException
DO 161      *             the server socket could not be opened.
162      * @throws IllegalStateException
163      *             the daemon is already running.
164      */
165     public synchronized void start() throws IOException {
166         if (run.get()) {
167             throw new IllegalStateException(JGitText.get().daemonAlreadyRunning);
168         }
169
924c9b 170         sshd.start();
af816d 171         run.set(true);
DO 172
2b5484 173         String sshBackendStr = gitblit.getSettings().getString(Keys.git.sshBackend,
JM 174                 SshSessionBackend.NIO2.name());
175
af816d 176         log.info(MessageFormat.format(
2b5484 177                 "SSH Daemon ({0}) is listening on {1}:{2,number,0}",
JM 178                 sshBackendStr, sshd.getHost(), sshd.getPort()));
af816d 179     }
DO 180
181     /** @return true if this daemon is receiving connections. */
182     public boolean isRunning() {
183         return run.get();
184     }
185
186     /** Stop this daemon. */
187     public synchronized void stop() {
188         if (run.get()) {
189             log.info("SSH Daemon stopping...");
190             run.set(false);
191
192             try {
23c416 193                 ((SshCommandFactory) sshd.getCommandFactory()).stop();
924c9b 194                 sshd.stop();
af816d 195             } catch (InterruptedException e) {
DO 196                 log.error("SSH Daemon stop interrupted", e);
197             }
b53611 198         }
JM 199     }
c910be 200
JM 201     private void generateKeyPair(File file, String algorithm, int keySize) {
202         if (file.exists()) {
203             return;
204         }
205         try {
206             KeyPairGenerator generator = SecurityUtils.getKeyPairGenerator(algorithm);
207             if (keySize != 0) {
208                 generator.initialize(keySize);
209                 log.info("Generating {}-{} SSH host keypair...", algorithm, keySize);
210             } else {
211                 log.info("Generating {} SSH host keypair...", algorithm);
212             }
213             KeyPair kp = generator.generateKeyPair();
214
215             // create an empty file and set the permissions
216             Files.touch(file);
217             try {
218                 JnaUtils.setFilemode(file, JnaUtils.S_IRUSR | JnaUtils.S_IWUSR);
01ff0f 219             } catch (UnsatisfiedLinkError | UnsupportedOperationException e) {
JM 220                 // Unexpected/Unsupported OS or Architecture
c910be 221             }
JM 222
223             FileOutputStream os = new FileOutputStream(file);
224             PEMWriter w = new PEMWriter(new OutputStreamWriter(os));
225             w.writeObject(kp);
226             w.flush();
227             w.close();
228         } catch (Exception e) {
229             log.warn(MessageFormat.format("Unable to generate {0} keypair", algorithm), e);
230             return;
231         }
232     }
af816d 233 }