James Moger
2015-11-22 ed552ba47c02779c270ffd62841d6d1048dade70
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
bf4fc5 28 import org.apache.sshd.common.io.IoServiceFactoryFactory;
DO 29 import org.apache.sshd.common.io.mina.MinaServiceFactoryFactory;
30 import org.apache.sshd.common.io.nio2.Nio2ServiceFactoryFactory;
c910be 31 import org.apache.sshd.common.util.SecurityUtils;
d41034 32 import org.apache.sshd.server.SshServer;
b3aabb 33 import org.apache.sshd.server.auth.CachingPublicKeyAuthenticator;
c910be 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
7a273c 101         SshKeyAuthenticator keyAuthenticator =
JM 102                 new SshKeyAuthenticator(gitblit.getPublicKeyManager(), gitblit);
c910be 103
JM 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         }
b3aabb 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);
7a273c 128         sshd.setPublickeyAuthenticator(new CachingPublicKeyAuthenticator(keyAuthenticator));
448145 129         sshd.setPasswordAuthenticator(new UsernamePasswordAuthenticator(gitblit));
92ae83 130         if (settings.getBoolean(Keys.git.sshWithKrb5, false)) {
JM 131             sshd.setGSSAuthenticator(new SshKrbAuthenticator(settings, gitblit));
5485da 132         }
61b32f 133         sshd.setSessionFactory(new SshServerSessionFactory());
924c9b 134         sshd.setFileSystemFactory(new DisabledFilesystemFactory());
9ba6bc 135         sshd.setTcpipForwardingFilter(new NonForwardingFilter());
5bb55f 136         sshd.setCommandFactory(new SshCommandFactory(gitblit, workQueue));
e725e1 137         sshd.setShellFactory(new WelcomeShell(settings));
JM 138
c910be 139         // Set the server id.  This can be queried with:
JM 140         //   ssh-keyscan -t rsa,dsa -p 29418 localhost
141         String version = String.format("%s (%s-%s)", Constants.getGitBlitVersion().replace(' ', '_'),
142                 sshd.getVersion(), sshBackendStr);
e725e1 143         sshd.getProperties().put(SshServer.SERVER_IDENTIFICATION, version);
af816d 144
DO 145         run = new AtomicBoolean(false);
146     }
147
148     public String formatUrl(String gituser, String servername, String repository) {
182312 149         IStoredSettings settings = gitblit.getSettings();
MB 150
151         int port = sshd.getPort();
b3aabb 152         int displayPort = settings.getInteger(Keys.git.sshAdvertisedPort, port);
JM 153         String displayServername = settings.getString(Keys.git.sshAdvertisedHost, "");
182312 154         if(displayServername.isEmpty()) {
MB 155             displayServername = servername;
156         }
157         if (displayPort == DEFAULT_PORT) {
af816d 158             // standard port
182312 159             return MessageFormat.format("ssh://{0}@{1}/{2}", gituser, displayServername,
af816d 160                     repository);
DO 161         } else {
162             // non-standard port
163             return MessageFormat.format("ssh://{0}@{1}:{2,number,0}/{3}",
182312 164                     gituser, displayServername, displayPort, repository);
af816d 165         }
DO 166     }
167
168     /**
169      * Start this daemon on a background thread.
924c9b 170      *
af816d 171      * @throws IOException
DO 172      *             the server socket could not be opened.
173      * @throws IllegalStateException
174      *             the daemon is already running.
175      */
176     public synchronized void start() throws IOException {
177         if (run.get()) {
178             throw new IllegalStateException(JGitText.get().daemonAlreadyRunning);
179         }
180
924c9b 181         sshd.start();
af816d 182         run.set(true);
DO 183
2b5484 184         String sshBackendStr = gitblit.getSettings().getString(Keys.git.sshBackend,
JM 185                 SshSessionBackend.NIO2.name());
186
af816d 187         log.info(MessageFormat.format(
2b5484 188                 "SSH Daemon ({0}) is listening on {1}:{2,number,0}",
JM 189                 sshBackendStr, sshd.getHost(), sshd.getPort()));
af816d 190     }
DO 191
192     /** @return true if this daemon is receiving connections. */
193     public boolean isRunning() {
194         return run.get();
195     }
196
197     /** Stop this daemon. */
198     public synchronized void stop() {
199         if (run.get()) {
200             log.info("SSH Daemon stopping...");
201             run.set(false);
202
203             try {
23c416 204                 ((SshCommandFactory) sshd.getCommandFactory()).stop();
924c9b 205                 sshd.stop();
d41034 206             } catch (IOException e) {
af816d 207                 log.error("SSH Daemon stop interrupted", e);
DO 208             }
b53611 209         }
JM 210     }
c910be 211
JM 212     private void generateKeyPair(File file, String algorithm, int keySize) {
213         if (file.exists()) {
214             return;
215         }
216         try {
217             KeyPairGenerator generator = SecurityUtils.getKeyPairGenerator(algorithm);
218             if (keySize != 0) {
219                 generator.initialize(keySize);
220                 log.info("Generating {}-{} SSH host keypair...", algorithm, keySize);
221             } else {
222                 log.info("Generating {} SSH host keypair...", algorithm);
223             }
224             KeyPair kp = generator.generateKeyPair();
225
226             // create an empty file and set the permissions
227             Files.touch(file);
228             try {
229                 JnaUtils.setFilemode(file, JnaUtils.S_IRUSR | JnaUtils.S_IWUSR);
01ff0f 230             } catch (UnsatisfiedLinkError | UnsupportedOperationException e) {
JM 231                 // Unexpected/Unsupported OS or Architecture
c910be 232             }
JM 233
234             FileOutputStream os = new FileOutputStream(file);
235             PEMWriter w = new PEMWriter(new OutputStreamWriter(os));
236             w.writeObject(kp);
237             w.flush();
238             w.close();
239         } catch (Exception e) {
240             log.warn(MessageFormat.format("Unable to generate {0} keypair", algorithm), e);
241             return;
242         }
243     }
af816d 244 }