James Moger
2015-10-05 be49ef9b1b2ab0ee251085efd5930b6f99bbced9
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;
5485da 26 import java.util.ArrayList;
FB 27 import java.util.List;
28 import java.util.Locale;
af816d 29 import java.util.concurrent.atomic.AtomicBoolean;
DO 30
5485da 31 import org.apache.sshd.common.NamedFactory;
bf4fc5 32 import org.apache.sshd.common.io.IoServiceFactoryFactory;
DO 33 import org.apache.sshd.common.io.mina.MinaServiceFactoryFactory;
34 import org.apache.sshd.common.io.nio2.Nio2ServiceFactoryFactory;
c910be 35 import org.apache.sshd.common.util.SecurityUtils;
d41034 36 import org.apache.sshd.server.SshServer;
b3aabb 37 import org.apache.sshd.server.auth.CachingPublicKeyAuthenticator;
d41034 38 import org.apache.sshd.server.auth.UserAuth;
JM 39 import org.apache.sshd.server.auth.UserAuthKeyboardInteractiveFactory;
40 import org.apache.sshd.server.auth.UserAuthPasswordFactory;
41 import org.apache.sshd.server.auth.UserAuthPublicKeyFactory;
5485da 42 import org.apache.sshd.server.auth.gss.GSSAuthenticator;
d41034 43 import org.apache.sshd.server.auth.gss.UserAuthGSSFactory;
c910be 44 import org.bouncycastle.openssl.PEMWriter;
af816d 45 import org.eclipse.jgit.internal.JGitText;
DO 46 import org.slf4j.Logger;
47 import org.slf4j.LoggerFactory;
48
e725e1 49 import com.gitblit.Constants;
af816d 50 import com.gitblit.IStoredSettings;
DO 51 import com.gitblit.Keys;
52 import com.gitblit.manager.IGitblit;
261ddf 53 import com.gitblit.transport.ssh.commands.SshCommandFactory;
c910be 54 import com.gitblit.utils.JnaUtils;
af816d 55 import com.gitblit.utils.StringUtils;
5bb55f 56 import com.gitblit.utils.WorkQueue;
c910be 57 import com.google.common.io.Files;
af816d 58
DO 59 /**
60  * Manager for the ssh transport. Roughly analogous to the
31f477 61  * {@link com.gitblit.transport.git.GitDaemon} class.
924c9b 62  *
af816d 63  */
924c9b 64 public class SshDaemon {
af816d 65
DO 66     private final Logger log = LoggerFactory.getLogger(SshDaemon.class);
67
bf4fc5 68     public static enum SshSessionBackend {
DO 69         MINA, NIO2
70     }
8982e6 71
af816d 72     /**
DO 73      * 22: IANA assigned port number for ssh. Note that this is a distinct
74      * concept from gitblit's default conf for ssh port -- this "default" is
75      * what the git protocol itself defaults to if it sees and ssh url without a
76      * port.
77      */
78     public static final int DEFAULT_PORT = 22;
79
924c9b 80     private final AtomicBoolean run;
af816d 81
924c9b 82     private final IGitblit gitblit;
JM 83     private final SshServer sshd;
af816d 84
DO 85     /**
86      * Construct the Gitblit SSH daemon.
924c9b 87      *
af816d 88      * @param gitblit
5bb55f 89      * @param workQueue
af816d 90      */
5bb55f 91     public SshDaemon(IGitblit gitblit, WorkQueue workQueue) {
af816d 92         this.gitblit = gitblit;
8982e6 93
af816d 94         IStoredSettings settings = gitblit.getSettings();
bf4fc5 95
c910be 96         // Ensure that Bouncy Castle is our JCE provider
JM 97         SecurityUtils.setRegisterBouncyCastle(true);
5604f3 98         if (SecurityUtils.isBouncyCastleRegistered()) {
JM 99             log.debug("BouncyCastle is registered as a JCE provider");
100         }
c910be 101
JM 102         // Generate host RSA and DSA keypairs and create the host keypair provider
103         File rsaKeyStore = new File(gitblit.getBaseFolder(), "ssh-rsa-hostkey.pem");
104         File dsaKeyStore = new File(gitblit.getBaseFolder(), "ssh-dsa-hostkey.pem");
105         generateKeyPair(rsaKeyStore, "RSA", 2048);
106         generateKeyPair(dsaKeyStore, "DSA", 0);
107         FileKeyPairProvider hostKeyPairProvider = new FileKeyPairProvider();
108         hostKeyPairProvider.setFiles(new String [] { rsaKeyStore.getPath(), dsaKeyStore.getPath(), dsaKeyStore.getPath() });
109
110         // Client public key authenticator
7a273c 111         SshKeyAuthenticator keyAuthenticator =
JM 112                 new SshKeyAuthenticator(gitblit.getPublicKeyManager(), gitblit);
c910be 113
JM 114         // Configure the preferred SSHD backend
bf4fc5 115         String sshBackendStr = settings.getString(Keys.git.sshBackend,
DO 116                 SshSessionBackend.NIO2.name());
117         SshSessionBackend backend = SshSessionBackend.valueOf(sshBackendStr);
118         System.setProperty(IoServiceFactoryFactory.class.getName(),
119             backend == SshSessionBackend.MINA
120                 ? MinaServiceFactoryFactory.class.getName()
121                 : Nio2ServiceFactoryFactory.class.getName());
8982e6 122
c910be 123         // Create the socket address for binding the SSH server
JM 124         int port = settings.getInteger(Keys.git.sshPort, 0);
125         String bindInterface = settings.getString(Keys.git.sshBindInterface, "");
924c9b 126         InetSocketAddress addr;
af816d 127         if (StringUtils.isEmpty(bindInterface)) {
924c9b 128             addr = new InetSocketAddress(port);
af816d 129         } else {
924c9b 130             addr = new InetSocketAddress(bindInterface, port);
af816d 131         }
b3aabb 132
5485da 133         //Will do GSS ?
FB 134         GSSAuthenticator gssAuthenticator = null;
135         if(settings.getBoolean(Keys.git.sshWithKrb5, false)) {
be49ef 136             gssAuthenticator = new SshKrbAuthenticator(gitblit, settings);
5485da 137             String keytabString = settings.getString(Keys.git.sshKrb5Keytab,
FB 138                     "");
139             if(! keytabString.isEmpty()) {
140                 gssAuthenticator.setKeytabFile(keytabString);
141             }
142             String servicePrincipalName = settings.getString(Keys.git.sshKrb5ServicePrincipalName,
143                     "");
144             if(! servicePrincipalName.isEmpty()) {
145                 gssAuthenticator.setServicePrincipalName(servicePrincipalName);
b3aabb 146             }
5485da 147         }
b3aabb 148
5485da 149         //Sort the authenticators for sshd
FB 150         List<NamedFactory<UserAuth>> userAuthFactories = new ArrayList<>();
151         String sshAuthenticatorsOrderString = settings.getString(Keys.git.sshAuthenticatorsOrder,
152                 "password,keyboard-interactive,publickey");
153         for(String authenticator: sshAuthenticatorsOrderString.split(",")) {
154             String authenticatorName = authenticator.trim().toLowerCase(Locale.US);
155             switch (authenticatorName) {
156             case "gssapi-with-mic":
157                 if(gssAuthenticator != null) {
d41034 158                     userAuthFactories.add(new UserAuthGSSFactory());
5485da 159                 }
FB 160                 break;
161             case "publickey":
d41034 162                 userAuthFactories.add(new UserAuthPublicKeyFactory());
5485da 163                 break;
FB 164             case "password":
d41034 165                 userAuthFactories.add(new UserAuthPasswordFactory());
5485da 166                 break;
FB 167             case "keyboard-interactive":
d41034 168                 userAuthFactories.add(new UserAuthKeyboardInteractiveFactory());
5485da 169                 break;
FB 170             default:
171                 log.error("Unknown ssh authenticator: '{}'", authenticatorName);
172             }
173         }
b3aabb 174
c910be 175         // Create the SSH server
924c9b 176         sshd = SshServer.setUpDefaultServer();
JM 177         sshd.setPort(addr.getPort());
178         sshd.setHost(addr.getHostName());
c910be 179         sshd.setKeyPairProvider(hostKeyPairProvider);
7a273c 180         sshd.setPublickeyAuthenticator(new CachingPublicKeyAuthenticator(keyAuthenticator));
448145 181         sshd.setPasswordAuthenticator(new UsernamePasswordAuthenticator(gitblit));
5485da 182         if(gssAuthenticator != null) {
FB 183             sshd.setGSSAuthenticator(gssAuthenticator);
184         }
185         sshd.setUserAuthFactories(userAuthFactories);
61b32f 186         sshd.setSessionFactory(new SshServerSessionFactory());
924c9b 187         sshd.setFileSystemFactory(new DisabledFilesystemFactory());
9ba6bc 188         sshd.setTcpipForwardingFilter(new NonForwardingFilter());
5bb55f 189         sshd.setCommandFactory(new SshCommandFactory(gitblit, workQueue));
e725e1 190         sshd.setShellFactory(new WelcomeShell(settings));
JM 191
c910be 192         // Set the server id.  This can be queried with:
JM 193         //   ssh-keyscan -t rsa,dsa -p 29418 localhost
194         String version = String.format("%s (%s-%s)", Constants.getGitBlitVersion().replace(' ', '_'),
195                 sshd.getVersion(), sshBackendStr);
e725e1 196         sshd.getProperties().put(SshServer.SERVER_IDENTIFICATION, version);
af816d 197
DO 198         run = new AtomicBoolean(false);
199     }
200
201     public String formatUrl(String gituser, String servername, String repository) {
182312 202         IStoredSettings settings = gitblit.getSettings();
MB 203
204         int port = sshd.getPort();
b3aabb 205         int displayPort = settings.getInteger(Keys.git.sshAdvertisedPort, port);
JM 206         String displayServername = settings.getString(Keys.git.sshAdvertisedHost, "");
182312 207         if(displayServername.isEmpty()) {
MB 208             displayServername = servername;
209         }
210         if (displayPort == DEFAULT_PORT) {
af816d 211             // standard port
182312 212             return MessageFormat.format("ssh://{0}@{1}/{2}", gituser, displayServername,
af816d 213                     repository);
DO 214         } else {
215             // non-standard port
216             return MessageFormat.format("ssh://{0}@{1}:{2,number,0}/{3}",
182312 217                     gituser, displayServername, displayPort, repository);
af816d 218         }
DO 219     }
220
221     /**
222      * Start this daemon on a background thread.
924c9b 223      *
af816d 224      * @throws IOException
DO 225      *             the server socket could not be opened.
226      * @throws IllegalStateException
227      *             the daemon is already running.
228      */
229     public synchronized void start() throws IOException {
230         if (run.get()) {
231             throw new IllegalStateException(JGitText.get().daemonAlreadyRunning);
232         }
233
924c9b 234         sshd.start();
af816d 235         run.set(true);
DO 236
2b5484 237         String sshBackendStr = gitblit.getSettings().getString(Keys.git.sshBackend,
JM 238                 SshSessionBackend.NIO2.name());
239
af816d 240         log.info(MessageFormat.format(
2b5484 241                 "SSH Daemon ({0}) is listening on {1}:{2,number,0}",
JM 242                 sshBackendStr, sshd.getHost(), sshd.getPort()));
af816d 243     }
DO 244
245     /** @return true if this daemon is receiving connections. */
246     public boolean isRunning() {
247         return run.get();
248     }
249
250     /** Stop this daemon. */
251     public synchronized void stop() {
252         if (run.get()) {
253             log.info("SSH Daemon stopping...");
254             run.set(false);
255
256             try {
23c416 257                 ((SshCommandFactory) sshd.getCommandFactory()).stop();
924c9b 258                 sshd.stop();
d41034 259             } catch (IOException e) {
af816d 260                 log.error("SSH Daemon stop interrupted", e);
DO 261             }
b53611 262         }
JM 263     }
c910be 264
JM 265     private void generateKeyPair(File file, String algorithm, int keySize) {
266         if (file.exists()) {
267             return;
268         }
269         try {
270             KeyPairGenerator generator = SecurityUtils.getKeyPairGenerator(algorithm);
271             if (keySize != 0) {
272                 generator.initialize(keySize);
273                 log.info("Generating {}-{} SSH host keypair...", algorithm, keySize);
274             } else {
275                 log.info("Generating {} SSH host keypair...", algorithm);
276             }
277             KeyPair kp = generator.generateKeyPair();
278
279             // create an empty file and set the permissions
280             Files.touch(file);
281             try {
282                 JnaUtils.setFilemode(file, JnaUtils.S_IRUSR | JnaUtils.S_IWUSR);
01ff0f 283             } catch (UnsatisfiedLinkError | UnsupportedOperationException e) {
JM 284                 // Unexpected/Unsupported OS or Architecture
c910be 285             }
JM 286
287             FileOutputStream os = new FileOutputStream(file);
288             PEMWriter w = new PEMWriter(new OutputStreamWriter(os));
289             w.writeObject(kp);
290             w.flush();
291             w.close();
292         } catch (Exception e) {
293             log.warn(MessageFormat.format("Unable to generate {0} keypair", algorithm), e);
294             return;
295         }
296     }
af816d 297 }