diff --git a/build.moxie b/build.moxie
index bd1c92880..86171165d 100644
--- a/build.moxie
+++ b/build.moxie
@@ -114,8 +114,7 @@ properties: {
bouncycastle.version : 1.81
selenium.version : 2.28.0
wikitext.version : 1.4
- sshd.version: 1.7.0
- mina.version: 2.0.27
+ sshd.version: 2.14.0
guice.version : 5.1.0
# Gitblit maintains a fork of guice-servlet
guice-servlet.version : 5.1.0-gb2
@@ -173,7 +172,6 @@ dependencies:
- compile 'org.bouncycastle:bcpkix-jdk18on:${bouncycastle.version}' :war
- compile 'net.i2p.crypto:eddsa:0.2.0' :war !org.easymock
- compile 'org.apache.sshd:sshd-core:${sshd.version}' :war !org.easymock
-- compile 'org.apache.mina:mina-core:${mina.version}' :war !org.easymock
- compile 'rome:rome:0.9' :war :manager :api
- compile 'com.google.code.gson:gson:2.10' :war :fedclient :manager :api
- compile 'org.codehaus.groovy:groovy-all:${groovy.version}' :war
diff --git a/src/main/java/com/gitblit/transport/ssh/DisabledFilesystemFactory.java b/src/main/java/com/gitblit/transport/ssh/DisabledFilesystemFactory.java
index 9bab3b8ee..789be6d7d 100644
--- a/src/main/java/com/gitblit/transport/ssh/DisabledFilesystemFactory.java
+++ b/src/main/java/com/gitblit/transport/ssh/DisabledFilesystemFactory.java
@@ -17,21 +17,20 @@
import java.io.IOException;
import java.nio.file.FileSystem;
+import java.nio.file.Path;
import org.apache.sshd.common.file.FileSystemFactory;
-import org.apache.sshd.common.session.Session;
+import org.apache.sshd.common.session.SessionContext;
public class DisabledFilesystemFactory implements FileSystemFactory {
- /**
- * Create user specific file system.
- *
- * @param session The session created for the user
- * @return The current {@link FileSystem} for the provided session
- * @throws java.io.IOException when the filesystem can not be created
- */
@Override
- public FileSystem createFileSystem(Session session) throws IOException {
- return null;
+ public Path getUserHomeDir(SessionContext session) throws IOException {
+ return null;
+ }
+
+ @Override
+ public FileSystem createFileSystem(SessionContext session) throws IOException {
+ return null;
}
}
diff --git a/src/main/java/com/gitblit/transport/ssh/FileKeyPairProvider.java b/src/main/java/com/gitblit/transport/ssh/FileKeyPairProvider.java
index 0e97f557c..336a48dcd 100644
--- a/src/main/java/com/gitblit/transport/ssh/FileKeyPairProvider.java
+++ b/src/main/java/com/gitblit/transport/ssh/FileKeyPairProvider.java
@@ -1,20 +1,17 @@
/*
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements. See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership. The ASF licenses this file
- * to you 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
+ * Copyright 2014 gitblit.com.
*
- * http://www.apache.org/licenses/LICENSE-2.0
+ * 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
*
- * 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.
+ * 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;
@@ -29,8 +26,10 @@
import java.util.Iterator;
import java.util.NoSuchElementException;
+import org.apache.sshd.common.config.keys.FilePasswordProvider;
import org.apache.sshd.common.config.keys.PrivateKeyEntryDecoder;
import org.apache.sshd.common.keyprovider.AbstractKeyPairProvider;
+import org.apache.sshd.common.session.SessionContext;
import org.apache.sshd.common.util.security.SecurityUtils;
import org.bouncycastle.openssl.PEMKeyPair;
import org.bouncycastle.openssl.PEMParser;
@@ -45,82 +44,62 @@
import org.bouncycastle.util.io.pem.PemObject;
import org.bouncycastle.util.io.pem.PemReader;
-/**
- * This host key provider loads private keys from the specified files.
- *
- * Note that this class has a direct dependency on BouncyCastle and won't work
- * unless it has been correctly registered as a security provider.
- *
- * @author Apache MINA SSHD Project
- */
-public class FileKeyPairProvider extends AbstractKeyPairProvider
-{
+public class FileKeyPairProvider extends AbstractKeyPairProvider {
private String[] files;
- public FileKeyPairProvider()
- {
+ public FileKeyPairProvider() {
}
- public FileKeyPairProvider(String[] files)
- {
+ public FileKeyPairProvider(String[] files) {
this.files = files;
}
- public String[] getFiles()
- {
+ public String[] getFiles() {
return files;
}
- public void setFiles(String[] files)
- {
+ public void setFiles(String[] files) {
this.files = files;
}
+ public Iterable loadKeys() {
+ return loadKeys(null);
+ }
@Override
- public Iterable loadKeys()
- {
+ public Iterable loadKeys(SessionContext session) {
if (!SecurityUtils.isBouncyCastleRegistered()) {
throw new IllegalStateException("BouncyCastle must be registered as a JCE provider");
}
- return new Iterable()
- {
+ return new Iterable() {
@Override
- public Iterator iterator()
- {
- return new Iterator()
- {
+ public Iterator iterator() {
+ return new Iterator() {
private final Iterator iterator = Arrays.asList(files).iterator();
private KeyPair nextKeyPair;
- private boolean nextKeyPairSet = false;
+ private boolean nextKeyPairSet;
@Override
- public boolean hasNext()
- {
+ public boolean hasNext() {
return nextKeyPairSet || setNextObject();
}
@Override
- public KeyPair next()
- {
- if (!nextKeyPairSet) {
- if (!setNextObject()) {
- throw new NoSuchElementException();
- }
+ public KeyPair next() {
+ if (!nextKeyPairSet && !setNextObject()) {
+ throw new NoSuchElementException();
}
nextKeyPairSet = false;
return nextKeyPair;
}
@Override
- public void remove()
- {
+ public void remove() {
throw new UnsupportedOperationException();
}
- private boolean setNextObject()
- {
+ private boolean setNextObject() {
while (iterator.hasNext()) {
String file = iterator.next();
File f = new File(file);
@@ -136,80 +115,65 @@ private boolean setNextObject()
}
return false;
}
-
};
}
};
}
-
- private KeyPair doLoadKey(String file)
- {
+ private KeyPair doLoadKey(String file) {
try {
-
try (PemReader r = new PemReader(new InputStreamReader(new FileInputStream(file)))) {
PemObject pemObject = r.readPemObject();
- if ("OPENSSH PRIVATE KEY".equals(pemObject.getType())) {
- // This reads a properly OpenSSH formatted ed25519 private key file.
- // It is currently unused because the SSHD library in play doesn't work with proper keys.
- // This is kept in the hope that in the future the library offers proper support.
+ if (pemObject != null && "OPENSSH PRIVATE KEY".equals(pemObject.getType())) {
try {
byte[] privateKeyContent = pemObject.getContent();
AsymmetricKeyParameter privateKeyParameters = OpenSSHPrivateKeyUtil.parsePrivateKeyBlob(privateKeyContent);
if (privateKeyParameters instanceof Ed25519PrivateKeyParameters) {
OpenSSHPrivateKeySpec privkeySpec = new OpenSSHPrivateKeySpec(privateKeyContent);
-
- Ed25519PublicKeyParameters publicKeyParameters = ((Ed25519PrivateKeyParameters)privateKeyParameters).generatePublicKey();
- OpenSSHPublicKeySpec pubKeySpec = new OpenSSHPublicKeySpec(OpenSSHPublicKeyUtil.encodePublicKey(publicKeyParameters));
-
+ Ed25519PublicKeyParameters publicKeyParameters =
+ ((Ed25519PrivateKeyParameters) privateKeyParameters).generatePublicKey();
+ OpenSSHPublicKeySpec pubKeySpec =
+ new OpenSSHPublicKeySpec(OpenSSHPublicKeyUtil.encodePublicKey(publicKeyParameters));
KeyFactory kf = KeyFactory.getInstance("Ed25519", "BC");
PrivateKey privateKey = kf.generatePrivate(privkeySpec);
PublicKey publicKey = kf.generatePublic(pubKeySpec);
return new KeyPair(publicKey, privateKey);
}
- else {
- log.warn("OpenSSH format is only supported for Ed25519 key type. Unable to read key " + file);
- }
- }
- catch (Exception e) {
- log.warn("Unable to read key " + file, e);
+ log.warn("OpenSSH format is only supported for Ed25519 key type. Unable to read key {}", file);
+ } catch (Exception e) {
+ log.warn("Unable to read key {}", file, e);
}
return null;
}
- if ("EDDSA PRIVATE KEY".equals(pemObject.getType())) {
- // This reads the ed25519 key from a file format that we created in SshDaemon.
- // The type EDDSA PRIVATE KEY was given by us and nothing official.
+ if (pemObject != null && "EDDSA PRIVATE KEY".equals(pemObject.getType())) {
byte[] privateKeyContent = pemObject.getContent();
- PrivateKeyEntryDecoder extends PublicKey,? extends PrivateKey> decoder = SecurityUtils.getOpenSSHEDDSAPrivateKeyEntryDecoder();
- PrivateKey privateKey = decoder.decodePrivateKey(null, privateKeyContent, 0, privateKeyContent.length);
- PublicKey publicKey = SecurityUtils. recoverEDDSAPublicKey(privateKey);
+ PrivateKeyEntryDecoder extends PublicKey, ? extends PrivateKey> decoder =
+ SecurityUtils.getOpenSSHEDDSAPrivateKeyEntryDecoder();
+ PrivateKey privateKey = decoder.decodePrivateKey(
+ null, FilePasswordProvider.EMPTY, privateKeyContent, 0, privateKeyContent.length);
+ PublicKey publicKey = SecurityUtils.recoverEDDSAPublicKey(privateKey);
return new KeyPair(publicKey, privateKey);
}
}
try (PEMParser r = new PEMParser(new InputStreamReader(new FileInputStream(file)))) {
Object o = r.readObject();
-
JcaPEMKeyConverter pemConverter = new JcaPEMKeyConverter();
pemConverter.setProvider("BC");
if (o instanceof PEMKeyPair) {
- o = pemConverter.getKeyPair((PEMKeyPair)o);
- return (KeyPair)o;
+ return pemConverter.getKeyPair((PEMKeyPair) o);
}
- else if (o instanceof KeyPair) {
- return (KeyPair)o;
+ if (o instanceof KeyPair) {
+ return (KeyPair) o;
}
- else {
- log.warn("Cannot read unsupported PEM object of type: " + o.getClass().getCanonicalName());
+ if (o != null) {
+ log.warn("Cannot read unsupported PEM object of type: {}", o.getClass().getCanonicalName());
}
}
-
- }
- catch (Exception e) {
- log.warn("Unable to read key " + file, e);
+ } catch (Exception e) {
+ log.warn("Unable to read key {}", file, e);
}
return null;
}
-
}
diff --git a/src/main/java/com/gitblit/transport/ssh/LdapKeyManager.java b/src/main/java/com/gitblit/transport/ssh/LdapKeyManager.java
index 9b494027c..37fb8772e 100644
--- a/src/main/java/com/gitblit/transport/ssh/LdapKeyManager.java
+++ b/src/main/java/com/gitblit/transport/ssh/LdapKeyManager.java
@@ -30,9 +30,9 @@
import org.apache.sshd.common.config.keys.KeyUtils;
import org.apache.sshd.common.util.GenericUtils;
+import com.gitblit.Constants.AccessPermission;
import com.gitblit.IStoredSettings;
import com.gitblit.Keys;
-import com.gitblit.Constants.AccessPermission;
import com.gitblit.ldap.LdapConnection;
import com.gitblit.models.UserModel;
import com.gitblit.utils.StringUtils;
@@ -43,393 +43,273 @@
import com.unboundid.ldap.sdk.SearchResult;
import com.unboundid.ldap.sdk.SearchResultEntry;
-/**
- * LDAP-only public key manager
- *
- * Retrieves public keys from user's LDAP entries. Using this key manager,
- * no SSH keys can be edited, i.e. added, removed, permissions changed, etc.
- *
- * This key manager supports SSH key entries in LDAP of the following form:
- * [:] [] []
- * This follows the required form of entries in the authenticated_keys file,
- * with an additional optional prefix. Key entries must have a key type
- * (like "ssh-rsa") and a key, and may have a comment at the end.
- *
- * An entry may specify login options as specified for the authorized_keys file.
- * The 'environment' option may be used to set the permissions for the key
- * by setting a 'gbPerm' environment variable. The key manager will interpret
- * such a environment variable option and use the set permission string to set
- * the permission on the key in Gitblit. Example:
- * environment="gbPerm=V",pty ssh-rsa AAAxjka.....dv= Clone only key
- * Above entry would create a RSA key with the comment "Clone only key" and
- * set the key permission to CLONE. All other options are ignored.
- *
- * In Active Directory SSH public keys are sometimes stored in the attribute
- * 'altSecurityIdentity'. The attribute value is usually prefixed by a type
- * identifier. LDAP entries could have the following attribute values:
- * altSecurityIdentity: X.509: ADKEJBAKDBZUPABBD...
- * altSecurityIdentity: SshKey: ssh-dsa AAAAknenazuzucbhda...
- * This key manager supports this by allowing an optional prefix to identify
- * SSH keys. The prefix to be used should be set in the 'realm.ldap.sshPublicKey'
- * setting by separating it from the attribute name with a colon, e.g.:
- * realm.ldap.sshPublicKey = altSecurityIdentity:SshKey
- *
- * @author Florian Zschocke
- *
- */
public class LdapKeyManager extends IPublicKeyManager {
- /**
- * Pattern to find prefixes like 'SSHKey:' in key entries.
- * These prefixes describe the type of an altSecurityIdentity.
- * The pattern accepts anything but quote and colon up to the
- * first colon at the start of a string.
- */
- private static final Pattern PREFIX_PATTERN = Pattern.compile("^([^\":]+):");
- /**
- * Pattern to find the string describing Gitblit permissions for a SSH key.
- * The pattern matches on a string starting with 'gbPerm', matched case-insensitive,
- * followed by '=' with optional whitespace around it, followed by a string of
- * upper and lower case letters and '+' and '-' for the permission, which can optionally
- * be enclosed in '"' or '\"' (only the leading quote is matched in the pattern).
- * Only the group describing the permission is a capturing group.
- */
- private static final Pattern GB_PERM_PATTERN = Pattern.compile("(?i:gbPerm)\\s*=\\s*(?:\\\\\"|\")?\\s*([A-Za-z+-]+)");
-
-
- private final IStoredSettings settings;
-
-
-
- @Inject
- public LdapKeyManager(IStoredSettings settings) {
- this.settings = settings;
- }
-
-
- @Override
- public String toString() {
- return getClass().getSimpleName();
- }
-
- @Override
- public LdapKeyManager start() {
- log.info(toString());
- return this;
- }
-
- @Override
- public boolean isReady() {
- return true;
- }
-
- @Override
- public LdapKeyManager stop() {
- return this;
- }
-
- @Override
- protected boolean isStale(String username) {
- // always return true so we gets keys from LDAP every time
- return true;
- }
-
- @Override
- protected List getKeysImpl(String username) {
- try (LdapConnection conn = new LdapConnection(settings)) {
- if (conn.connect()) {
- log.info("loading ssh key for {} from LDAP directory", username);
-
- BindResult bindResult = conn.bind();
- if (bindResult == null) {
- conn.close();
- return null;
- }
-
- // Search the user entity
-
- // Support prefixing the key data, e.g. when using altSecurityIdentities in AD.
- String pubKeyAttribute = settings.getString(Keys.realm.ldap.sshPublicKey, "sshPublicKey");
- String pkaPrefix = null;
- int idx = pubKeyAttribute.indexOf(':');
- if (idx > 0) {
- pkaPrefix = pubKeyAttribute.substring(idx +1);
- pubKeyAttribute = pubKeyAttribute.substring(0, idx);
- }
-
- SearchResult result = conn.searchUser(getSimpleUsername(username), Arrays.asList(pubKeyAttribute));
- conn.close();
-
- if (result != null && result.getResultCode() == ResultCode.SUCCESS) {
- if ( result.getEntryCount() > 1) {
- log.info("Found more than one entry for user {} in LDAP. Cannot retrieve SSH key.", username);
- return null;
- } else if ( result.getEntryCount() < 1) {
- log.info("Found no entry for user {} in LDAP. Cannot retrieve SSH key.", username);
- return null;
- }
-
- // Retrieve the SSH key attributes
- SearchResultEntry foundUser = result.getSearchEntries().get(0);
- String[] attrs = foundUser.getAttributeValues(pubKeyAttribute);
- if (attrs == null ||attrs.length == 0) {
- log.info("found no keys for user {} under attribute {} in directory", username, pubKeyAttribute);
- return null;
- }
-
-
- // Filter resulting list to match with required special prefix in entry
- List authorizedKeys = new ArrayList<>(attrs.length);
- Matcher m = PREFIX_PATTERN.matcher("");
- for (int i = 0; i < attrs.length; ++i) {
- // strip out line breaks
- String keyEntry = Joiner.on("").join(attrs[i].replace("\r\n", "\n").split("\n"));
- m.reset(keyEntry);
- try {
- if (m.lookingAt()) { // Key is prefixed in LDAP
- if (pkaPrefix == null) {
- continue;
- }
- String prefix = m.group(1).trim();
- if (! pkaPrefix.equalsIgnoreCase(prefix)) {
- continue;
- }
- String s = keyEntry.substring(m.end()); // Strip prefix off
- authorizedKeys.add(GbAuthorizedKeyEntry.parseAuthorizedKeyEntry(s));
-
- } else { // Key is not prefixed in LDAP
- if (pkaPrefix != null) {
- continue;
- }
- String s = keyEntry; // Strip prefix off
- authorizedKeys.add(GbAuthorizedKeyEntry.parseAuthorizedKeyEntry(s));
- }
- } catch (IllegalArgumentException e) {
- log.info("Failed to parse key entry={}:", keyEntry, e.getMessage());
- }
- }
-
- List keyList = new ArrayList<>(authorizedKeys.size());
- for (GbAuthorizedKeyEntry keyEntry : authorizedKeys) {
- try {
- SshKey key = new SshKey(keyEntry.resolvePublicKey(null));
- key.setComment(keyEntry.getComment());
- setKeyPermissions(key, keyEntry);
- keyList.add(key);
- } catch (GeneralSecurityException | IOException e) {
- log.warn("Error resolving key entry for user {}. Entry={}", username, keyEntry, e);
- }
- }
- return keyList;
- }
- }
- }
-
- return null;
- }
-
-
- @Override
- public boolean addKey(String username, SshKey key) {
- return false;
- }
-
- @Override
- public boolean removeKey(String username, SshKey key) {
- return false;
- }
-
- @Override
- public boolean removeAllKeys(String username) {
- return false;
- }
-
-
- public boolean supportsWritingKeys(UserModel user) {
- return false;
- }
-
- public boolean supportsCommentChanges(UserModel user) {
- return false;
- }
-
- public boolean supportsPermissionChanges(UserModel user) {
- return false;
- }
-
-
- private void setKeyPermissions(SshKey key, GbAuthorizedKeyEntry keyEntry) {
- List env = keyEntry.getLoginOptionValues("environment");
- if (env != null && !env.isEmpty()) {
- // Walk over all entries and find one that sets 'gbPerm'. The last one wins.
- for (String envi : env) {
- Matcher m = GB_PERM_PATTERN.matcher(envi);
- if (m.find()) {
- String perm = m.group(1).trim();
- AccessPermission ap = AccessPermission.fromCode(perm);
- if (ap == AccessPermission.NONE) {
- ap = AccessPermission.valueOf(perm.toUpperCase());
- }
-
- if (ap != null && ap != AccessPermission.NONE) {
- try {
- key.setPermission(ap);
- } catch (IllegalArgumentException e) {
- log.warn("Incorrect permissions ({}) set for SSH key entry {}.", ap, envi, e);
- }
- }
- }
- }
- }
- }
-
-
- /**
- * Returns a simple username without any domain prefixes.
- *
- * @param username
- * @return a simple username
- */
- private String getSimpleUsername(String username) {
- int lastSlash = username.lastIndexOf('\\');
- if (lastSlash > -1) {
- username = username.substring(lastSlash + 1);
- }
-
- return username;
- }
-
-
- /**
- * Extension of the AuthorizedKeyEntry from Mina SSHD with better option parsing.
- *
- * The class makes use of code from the two methods copied from the original
- * Mina SSHD AuthorizedKeyEntry class. The code is rewritten to improve user login
- * option support. Options are correctly parsed even if they have whitespace within
- * double quotes. Options can occur multiple times, which is needed for example for
- * the "environment" option. Thus for an option a list of strings is kept, holding
- * multiple option values.
- */
- private static class GbAuthorizedKeyEntry extends AuthorizedKeyEntry {
-
- private static final long serialVersionUID = 1L;
- /**
- * Pattern to extract the first part of the key entry without whitespace or only with quoted whitespace.
- * The pattern essentially splits the line in two parts with two capturing groups. All other groups
- * in the pattern are non-capturing. The first part is a continuous string that only includes double quoted
- * whitespace and ends in whitespace. The second part is the rest of the line.
- * The first part is at the beginning of the line, the lead-in. For a SSH key entry this can either be
- * login options (see authorized keys file description) or the key type. Since options, other than the
- * key type, can include whitespace and escaped double quotes within double quotes, the pattern takes
- * care of that by searching for either "characters that are not whitespace and not double quotes"
- * or "a double quote, followed by 'characters that are not a double quote or backslash, or a backslash
- * and then a double quote, or a backslash', followed by a double quote".
- */
- private static final Pattern LEADIN_PATTERN = Pattern.compile("^((?:[^\\s\"]*|(?:\"(?:[^\"\\\\]|\\\\\"|\\\\)*\"))*\\s+)(.+)");
- /**
- * Pattern to split a comma separated list of options.
- * Since an option could contain commas (as well as escaped double quotes) within double quotes
- * in the option value, a simple split on comma is not enough. So the pattern searches for multiple
- * occurrences of:
- * characters that are not double quotes or a comma, or
- * a double quote followed by: characters that are not a double quote or backslash, or
- * a backslash and then a double quote, or
- * a backslash,
- * followed by a double quote.
- */
- private static final Pattern OPTION_PATTERN = Pattern.compile("([^\",]+|(?:\"(?:[^\"\\\\]|\\\\\"|\\\\)*\"))+");
-
- // for options that have no value, "true" is used
- private Map> loginOptionsMulti = Collections.emptyMap();
-
-
- List getLoginOptionValues(String option) {
- return loginOptionsMulti.get(option);
- }
-
-
-
- /**
- * @param line Original line from an authorized_keys file
- * @return {@link GbAuthorizedKeyEntry} or {@code null} if the line is
- * {@code null}/empty or a comment line
- * @throws IllegalArgumentException If failed to parse/decode the line
- * @see #COMMENT_CHAR
- */
- public static GbAuthorizedKeyEntry parseAuthorizedKeyEntry(String line) throws IllegalArgumentException {
- line = GenericUtils.trimToEmpty(line);
- if (StringUtils.isEmpty(line) || (line.charAt(0) == COMMENT_CHAR) /* comment ? */) {
- return null;
- }
-
- Matcher m = LEADIN_PATTERN.matcher(line);
- if (! m.lookingAt()) {
- throw new IllegalArgumentException("Bad format (no key data delimiter): " + line);
- }
-
- String keyType = m.group(1).trim();
- final GbAuthorizedKeyEntry entry;
- if (KeyUtils.getPublicKeyEntryDecoder(keyType) == null) { // assume this is due to the fact that it starts with login options
- entry = parseAuthorizedKeyEntry(m.group(2));
- if (entry == null) {
- throw new IllegalArgumentException("Bad format (no key data after login options): " + line);
- }
-
- entry.parseAndSetLoginOptions(keyType);
- } else {
- int startPos = line.indexOf(' ');
- if (startPos <= 0) {
- throw new IllegalArgumentException("Bad format (no key data delimiter): " + line);
- }
-
- int endPos = line.indexOf(' ', startPos + 1);
- if (endPos <= startPos) {
- endPos = line.length();
- }
-
- String encData = (endPos < (line.length() - 1)) ? line.substring(0, endPos).trim() : line;
- String comment = (endPos < (line.length() - 1)) ? line.substring(endPos + 1).trim() : null;
- entry = parsePublicKeyEntry(new GbAuthorizedKeyEntry(), encData);
- entry.setComment(comment);
- }
-
- return entry;
- }
-
- private void parseAndSetLoginOptions(String options) {
- Matcher m = OPTION_PATTERN.matcher(options);
- if (! m.find()) {
- loginOptionsMulti = Collections.emptyMap();
- }
- Map> optsMap = new TreeMap<>(String.CASE_INSENSITIVE_ORDER);
-
- do {
- String p = m.group();
- p = GenericUtils.trimToEmpty(p);
- if (StringUtils.isEmpty(p)) {
- continue;
- }
-
- int pos = p.indexOf('=');
- String name = (pos < 0) ? p : GenericUtils.trimToEmpty(p.substring(0, pos));
- CharSequence value = (pos < 0) ? null : GenericUtils.trimToEmpty(p.substring(pos + 1));
- value = GenericUtils.stripQuotes(value);
-
- // For options without value the value is set to TRUE.
- if (value == null) {
- value = Boolean.TRUE.toString();
- }
-
- List opts = optsMap.get(name);
- if (opts == null) {
- opts = new ArrayList();
- optsMap.put(name, opts);
- }
- opts.add(value.toString());
- } while(m.find());
-
- loginOptionsMulti = optsMap;
- }
- }
-
+ private static final Pattern PREFIX_PATTERN = Pattern.compile("^([^\":]+):");
+ private static final Pattern GB_PERM_PATTERN = Pattern.compile("(?i:gbPerm)\\s*=\\s*(?:\\\\\"|\")?\\s*([A-Za-z+-]+)");
+
+ private final IStoredSettings settings;
+
+ @Inject
+ public LdapKeyManager(IStoredSettings settings) {
+ this.settings = settings;
+ }
+
+ @Override
+ public String toString() {
+ return getClass().getSimpleName();
+ }
+
+ @Override
+ public LdapKeyManager start() {
+ log.info(toString());
+ return this;
+ }
+
+ @Override
+ public boolean isReady() {
+ return true;
+ }
+
+ @Override
+ public LdapKeyManager stop() {
+ return this;
+ }
+
+ @Override
+ protected boolean isStale(String username) {
+ return true;
+ }
+
+ @Override
+ protected List getKeysImpl(String username) {
+ try (LdapConnection conn = new LdapConnection(settings)) {
+ if (conn.connect()) {
+ log.info("loading ssh key for {} from LDAP directory", username);
+
+ BindResult bindResult = conn.bind();
+ if (bindResult == null) {
+ conn.close();
+ return null;
+ }
+
+ String pubKeyAttribute = settings.getString(Keys.realm.ldap.sshPublicKey, "sshPublicKey");
+ String pkaPrefix = null;
+ int idx = pubKeyAttribute.indexOf(':');
+ if (idx > 0) {
+ pkaPrefix = pubKeyAttribute.substring(idx + 1);
+ pubKeyAttribute = pubKeyAttribute.substring(0, idx);
+ }
+
+ SearchResult result = conn.searchUser(getSimpleUsername(username), Arrays.asList(pubKeyAttribute));
+ conn.close();
+
+ if (result != null && result.getResultCode() == ResultCode.SUCCESS) {
+ if (result.getEntryCount() > 1) {
+ log.info("Found more than one entry for user {} in LDAP. Cannot retrieve SSH key.", username);
+ return null;
+ } else if (result.getEntryCount() < 1) {
+ log.info("Found no entry for user {} in LDAP. Cannot retrieve SSH key.", username);
+ return null;
+ }
+
+ SearchResultEntry foundUser = result.getSearchEntries().get(0);
+ String[] attrs = foundUser.getAttributeValues(pubKeyAttribute);
+ if (attrs == null || attrs.length == 0) {
+ log.info("found no keys for user {} under attribute {} in directory", username, pubKeyAttribute);
+ return null;
+ }
+
+ List authorizedKeys = new ArrayList(attrs.length);
+ Matcher m = PREFIX_PATTERN.matcher("");
+ for (int i = 0; i < attrs.length; ++i) {
+ String keyEntry = Joiner.on("").join(attrs[i].replace("\r\n", "\n").split("\n"));
+ m.reset(keyEntry);
+ try {
+ if (m.lookingAt()) {
+ if (pkaPrefix == null) {
+ continue;
+ }
+ String prefix = m.group(1).trim();
+ if (!pkaPrefix.equalsIgnoreCase(prefix)) {
+ continue;
+ }
+ authorizedKeys.add(GbAuthorizedKeyEntry.parseAuthorizedKeyEntry(keyEntry.substring(m.end())));
+ } else {
+ if (pkaPrefix != null) {
+ continue;
+ }
+ authorizedKeys.add(GbAuthorizedKeyEntry.parseAuthorizedKeyEntry(keyEntry));
+ }
+ } catch (IllegalArgumentException e) {
+ log.info("Failed to parse key entry={}:", keyEntry, e.getMessage());
+ }
+ }
+
+ List keyList = new ArrayList(authorizedKeys.size());
+ for (GbAuthorizedKeyEntry keyEntry : authorizedKeys) {
+ try {
+ SshKey key = new SshKey(keyEntry.resolvePublicKey(null, null));
+ key.setComment(keyEntry.getComment());
+ setKeyPermissions(key, keyEntry);
+ keyList.add(key);
+ } catch (GeneralSecurityException | IOException e) {
+ log.warn("Error resolving key entry for user {}. Entry={}", username, keyEntry, e);
+ }
+ }
+ return keyList;
+ }
+ }
+ }
+
+ return null;
+ }
+
+ @Override
+ public boolean addKey(String username, SshKey key) {
+ return false;
+ }
+
+ @Override
+ public boolean removeKey(String username, SshKey key) {
+ return false;
+ }
+
+ @Override
+ public boolean removeAllKeys(String username) {
+ return false;
+ }
+
+ public boolean supportsWritingKeys(UserModel user) {
+ return false;
+ }
+
+ public boolean supportsCommentChanges(UserModel user) {
+ return false;
+ }
+
+ public boolean supportsPermissionChanges(UserModel user) {
+ return false;
+ }
+
+ private void setKeyPermissions(SshKey key, GbAuthorizedKeyEntry keyEntry) {
+ List env = keyEntry.getLoginOptionValues("environment");
+ if (env != null && !env.isEmpty()) {
+ for (String envi : env) {
+ Matcher m = GB_PERM_PATTERN.matcher(envi);
+ if (m.find()) {
+ String perm = m.group(1).trim();
+ AccessPermission ap = AccessPermission.fromCode(perm);
+ if (ap == AccessPermission.NONE) {
+ ap = AccessPermission.valueOf(perm.toUpperCase());
+ }
+
+ if (ap != null && ap != AccessPermission.NONE) {
+ try {
+ key.setPermission(ap);
+ } catch (IllegalArgumentException e) {
+ log.warn("Incorrect permissions ({}) set for SSH key entry {}.", ap, envi, e);
+ }
+ }
+ }
+ }
+ }
+ }
+
+ private String getSimpleUsername(String username) {
+ int lastSlash = username.lastIndexOf('\\');
+ if (lastSlash > -1) {
+ username = username.substring(lastSlash + 1);
+ }
+
+ return username;
+ }
+
+ private static class GbAuthorizedKeyEntry extends AuthorizedKeyEntry {
+
+ private static final long serialVersionUID = 1L;
+ private static final Pattern LEADIN_PATTERN = Pattern.compile("^((?:[^\\s\"]*|(?:\"(?:[^\"\\\\]|\\\\\"|\\\\)*\"))*\\s+)(.+)");
+ private static final Pattern OPTION_PATTERN = Pattern.compile("([^\",]+|(?:\"(?:[^\"\\\\]|\\\\\"|\\\\)*\"))+");
+
+ private Map> loginOptionsMulti = Collections.emptyMap();
+
+ List getLoginOptionValues(String option) {
+ return loginOptionsMulti.get(option);
+ }
+
+ public static GbAuthorizedKeyEntry parseAuthorizedKeyEntry(String line) throws IllegalArgumentException {
+ line = GenericUtils.trimToEmpty(line);
+ if (StringUtils.isEmpty(line) || (line.charAt(0) == COMMENT_CHAR)) {
+ return null;
+ }
+
+ Matcher m = LEADIN_PATTERN.matcher(line);
+ if (!m.lookingAt()) {
+ throw new IllegalArgumentException("Bad format (no key data delimiter): " + line);
+ }
+
+ String keyType = m.group(1).trim();
+ final GbAuthorizedKeyEntry entry;
+ if (KeyUtils.getPublicKeyEntryDecoder(keyType) == null) {
+ entry = parseAuthorizedKeyEntry(m.group(2));
+ if (entry == null) {
+ throw new IllegalArgumentException("Bad format (no key data after login options): " + line);
+ }
+
+ entry.parseAndSetLoginOptions(keyType);
+ } else {
+ int startPos = line.indexOf(' ');
+ if (startPos <= 0) {
+ throw new IllegalArgumentException("Bad format (no key data delimiter): " + line);
+ }
+
+ int endPos = line.indexOf(' ', startPos + 1);
+ if (endPos <= startPos) {
+ endPos = line.length();
+ }
+
+ String encData = (endPos < (line.length() - 1)) ? line.substring(0, endPos).trim() : line;
+ String comment = (endPos < (line.length() - 1)) ? line.substring(endPos + 1).trim() : null;
+ entry = parsePublicKeyEntry(new GbAuthorizedKeyEntry(), encData);
+ entry.setComment(comment);
+ }
+
+ return entry;
+ }
+
+ private void parseAndSetLoginOptions(String options) {
+ Matcher m = OPTION_PATTERN.matcher(options);
+ if (!m.find()) {
+ loginOptionsMulti = Collections.emptyMap();
+ return;
+ }
+ Map> optsMap = new TreeMap>(String.CASE_INSENSITIVE_ORDER);
+
+ do {
+ String p = m.group();
+ p = GenericUtils.trimToEmpty(p);
+ if (StringUtils.isEmpty(p)) {
+ continue;
+ }
+
+ int pos = p.indexOf('=');
+ String name = (pos < 0) ? p : GenericUtils.trimToEmpty(p.substring(0, pos));
+ CharSequence value = (pos < 0) ? null : GenericUtils.trimToEmpty(p.substring(pos + 1));
+ value = GenericUtils.stripQuotes(value);
+
+ if (value == null) {
+ value = Boolean.TRUE.toString();
+ }
+
+ List opts = optsMap.get(name);
+ if (opts == null) {
+ opts = new ArrayList();
+ optsMap.put(name, opts);
+ }
+ opts.add(value.toString());
+ } while (m.find());
+
+ loginOptionsMulti = optsMap;
+ }
+ }
}
diff --git a/src/main/java/com/gitblit/transport/ssh/ServerSigAlgsExtensionHandler.java b/src/main/java/com/gitblit/transport/ssh/ServerSigAlgsExtensionHandler.java
new file mode 100644
index 000000000..a14c4a19a
--- /dev/null
+++ b/src/main/java/com/gitblit/transport/ssh/ServerSigAlgsExtensionHandler.java
@@ -0,0 +1,118 @@
+/*
+ * Copyright 2026 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.util.Arrays;
+import java.util.Collection;
+import java.util.LinkedHashMap;
+import java.util.Map;
+import java.util.function.BiConsumer;
+
+import org.apache.sshd.common.AttributeRepository.AttributeKey;
+import org.apache.sshd.common.kex.KexProposalOption;
+import org.apache.sshd.common.kex.extension.KexExtensionHandler;
+import org.apache.sshd.common.kex.extension.KexExtensions;
+import org.apache.sshd.common.kex.extension.parser.ServerSignatureAlgorithms;
+import org.apache.sshd.common.session.Session;
+import org.apache.sshd.common.util.GenericUtils;
+import org.apache.sshd.common.util.buffer.Buffer;
+import org.apache.sshd.common.util.logging.AbstractLoggingBean;
+
+public class ServerSigAlgsExtensionHandler extends AbstractLoggingBean implements KexExtensionHandler {
+
+ public static final AttributeKey CLIENT_REQUESTED_EXT_INFO = new AttributeKey<>();
+ public static final AttributeKey EXT_INFO_SENT_AT_NEWKEYS = new AttributeKey<>();
+
+ @Override
+ public void handleKexInitProposal(Session session, boolean initiator, Map proposal)
+ throws Exception {
+ if (initiator) {
+ return;
+ }
+
+ if (session.getAttribute(CLIENT_REQUESTED_EXT_INFO) != null) {
+ return;
+ }
+
+ String algorithms = proposal.get(KexProposalOption.ALGORITHMS);
+ boolean clientWantsExtInfo = Arrays.asList(GenericUtils.split(algorithms, ','))
+ .contains(KexExtensions.CLIENT_KEX_EXTENSION);
+ session.setAttribute(CLIENT_REQUESTED_EXT_INFO, Boolean.valueOf(clientWantsExtInfo));
+ if (clientWantsExtInfo && log.isTraceEnabled()) {
+ log.trace("handleKexInitProposal({}): got ext-info-c from client", session);
+ }
+ }
+
+ @Override
+ public boolean isKexExtensionsAvailable(Session session, AvailabilityPhase phase) {
+ return phase != AvailabilityPhase.PREKEX;
+ }
+
+ @Override
+ public void sendKexExtensions(Session session, KexPhase phase) throws Exception {
+ if (phase == KexPhase.NEWKEYS) {
+ Boolean alreadySent = session.getAttribute(EXT_INFO_SENT_AT_NEWKEYS);
+ if (Boolean.TRUE.equals(alreadySent)) {
+ return;
+ }
+ session.setAttribute(EXT_INFO_SENT_AT_NEWKEYS, Boolean.TRUE);
+ }
+
+ Boolean doExtInfo = session.getAttribute(CLIENT_REQUESTED_EXT_INFO);
+ if (!Boolean.TRUE.equals(doExtInfo)) {
+ if (log.isTraceEnabled()) {
+ log.trace("sendKexExtensions({})[{}]: client did not send ext-info-c; skipping SSH_MSG_EXT_INFO",
+ session, phase);
+ }
+ return;
+ }
+
+ Map extensions = new LinkedHashMap<>();
+ collectExtensions(session, phase, extensions::put);
+ if (extensions.isEmpty()) {
+ if (log.isDebugEnabled()) {
+ log.debug("sendKexExtensions({})[{}]: no extension info; skipping SSH_MSG_EXT_INFO", session, phase);
+ }
+ return;
+ }
+
+ Buffer buffer = session.createBuffer(KexExtensions.SSH_MSG_EXT_INFO);
+ KexExtensions.putExtensions(extensions.entrySet(), buffer);
+ session.writePacket(buffer);
+ if (log.isDebugEnabled()) {
+ log.debug("sendKexExtensions({})[{}]: sending SSH_MSG_EXT_INFO with {} info records",
+ session, phase, Integer.valueOf(extensions.size()));
+ }
+ }
+
+ public void collectExtensions(Session session, KexPhase phase, BiConsumer marshaller) {
+ if (phase != KexPhase.NEWKEYS) {
+ return;
+ }
+
+ Collection algorithms = session.getSignatureFactoriesNames();
+ if (!GenericUtils.isEmpty(algorithms)) {
+ marshaller.accept(ServerSignatureAlgorithms.NAME, algorithms);
+ if (log.isDebugEnabled()) {
+ log.debug("collectExtensions({})[{}]: extension info {}: {}", session, phase,
+ ServerSignatureAlgorithms.NAME, String.join(",", algorithms));
+ }
+ } else if (log.isWarnEnabled()) {
+ log.warn("collectExtensions({})[{}]: extension info {} has no algorithms; skipping",
+ session, phase, ServerSignatureAlgorithms.NAME);
+ }
+ }
+}
diff --git a/src/main/java/com/gitblit/transport/ssh/SshDaemon.java b/src/main/java/com/gitblit/transport/ssh/SshDaemon.java
index d4f1fab01..9b14edf7c 100644
--- a/src/main/java/com/gitblit/transport/ssh/SshDaemon.java
+++ b/src/main/java/com/gitblit/transport/ssh/SshDaemon.java
@@ -15,7 +15,6 @@
*/
package com.gitblit.transport.ssh;
-import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
@@ -29,16 +28,17 @@
import java.util.concurrent.atomic.AtomicBoolean;
import net.i2p.crypto.eddsa.EdDSAPrivateKey;
+import net.i2p.crypto.eddsa.EdDSAPublicKey;
import org.apache.sshd.common.config.keys.KeyEntryResolver;
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.core.CoreModuleProperties;
+import org.apache.sshd.common.util.io.output.SecureByteArrayOutputStream;
import org.apache.sshd.common.util.security.SecurityUtils;
import org.apache.sshd.common.util.security.bouncycastle.BouncyCastleSecurityProviderRegistrar;
import org.apache.sshd.common.util.security.eddsa.EdDSASecurityProviderRegistrar;
import org.apache.sshd.common.util.security.eddsa.OpenSSHEd25519PrivateKeyEntryDecoder;
import org.apache.sshd.server.SshServer;
-import org.apache.sshd.server.auth.pubkey.CachingPublicKeyAuthenticator;
import org.bouncycastle.crypto.params.AsymmetricKeyParameter;
import org.bouncycastle.crypto.util.OpenSSHPrivateKeyUtil;
import org.bouncycastle.crypto.util.PrivateKeyFactory;
@@ -59,264 +59,216 @@
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 static final Logger log = LoggerFactory.getLogger(SshDaemon.class);
-
- private static final String AUTH_PUBLICKEY = "publickey";
- private static final String AUTH_PASSWORD = "password";
- private static final String AUTH_KBD_INTERACTIVE = "keyboard-interactive";
- private static final String AUTH_GSSAPI = "gssapi-with-mic";
-
-
-
- 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 static final Logger log = LoggerFactory.getLogger(SshDaemon.class);
- private final IGitblit gitblit;
- private final SshServer sshd;
+ private static final String AUTH_PUBLICKEY = "publickey";
+ private static final String AUTH_PASSWORD = "password";
+ private static final String AUTH_KBD_INTERACTIVE = "keyboard-interactive";
+ private static final String AUTH_GSSAPI = "gssapi-with-mic";
- /**
- * Construct the Gitblit SSH daemon.
- *
- * @param gitblit
- * @param workQueue
- */
- public SshDaemon(IGitblit gitblit, WorkQueue workQueue) {
- this.gitblit = gitblit;
-
- IStoredSettings settings = gitblit.getSettings();
+ public static enum SshSessionBackend {
+ MINA, NIO2
+ }
- // Ensure that Bouncy Castle is our JCE provider
- SecurityUtils.registerSecurityProvider(new BouncyCastleSecurityProviderRegistrar());
- if (SecurityUtils.isBouncyCastleRegistered()) {
- log.info("BouncyCastle is registered as a JCE provider");
- }
- // Add support for ED25519_SHA512
- SecurityUtils.registerSecurityProvider(new EdDSASecurityProviderRegistrar());
- if (SecurityUtils.isProviderRegistered("EdDSA")) {
- log.info("EdDSA is registered as a JCE provider");
- }
+ public static final int DEFAULT_PORT = 22;
- // 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");
- File ecdsaKeyStore = new File(gitblit.getBaseFolder(), "ssh-ecdsa-hostkey.pem");
- File eddsaKeyStore = new File(gitblit.getBaseFolder(), "ssh-eddsa-hostkey.pem");
- File ed25519KeyStore = new File(gitblit.getBaseFolder(), "ssh-ed25519-hostkey.pem");
- generateKeyPair(rsaKeyStore, "RSA", 2048);
- generateKeyPair(ecdsaKeyStore, "ECDSA", 256);
- generateKeyPair(eddsaKeyStore, "EdDSA", 0);
- FileKeyPairProvider hostKeyPairProvider = new FileKeyPairProvider();
- hostKeyPairProvider.setFiles(new String [] { ecdsaKeyStore.getPath(), eddsaKeyStore.getPath(), ed25519KeyStore.getPath(), rsaKeyStore.getPath(), dsaKeyStore.getPath() });
+ private final AtomicBoolean run;
+ private final IGitblit gitblit;
+ private final SshServer sshd;
+ public SshDaemon(IGitblit gitblit, WorkQueue workQueue) {
+ this.gitblit = 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());
+ IStoredSettings settings = gitblit.getSettings();
- // 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);
- }
+ SecurityUtils.registerSecurityProvider(new BouncyCastleSecurityProviderRegistrar());
+ if (SecurityUtils.isBouncyCastleRegistered()) {
+ log.info("BouncyCastle is registered as a JCE provider");
+ }
+ SecurityUtils.registerSecurityProvider(new EdDSASecurityProviderRegistrar());
+ if (SecurityUtils.isProviderRegistered("EdDSA")) {
+ log.info("EdDSA is registered as a JCE provider");
+ }
- // Create the SSH server
- sshd = SshServer.setUpDefaultServer();
- sshd.setPort(addr.getPort());
- sshd.setHost(addr.getHostName());
- sshd.setKeyPairProvider(hostKeyPairProvider);
+ File rsaKeyStore = new File(gitblit.getBaseFolder(), "ssh-rsa-hostkey.pem");
+ File dsaKeyStore = new File(gitblit.getBaseFolder(), "ssh-dsa-hostkey.pem");
+ File ecdsaKeyStore = new File(gitblit.getBaseFolder(), "ssh-ecdsa-hostkey.pem");
+ File eddsaKeyStore = new File(gitblit.getBaseFolder(), "ssh-eddsa-hostkey.pem");
+ File ed25519KeyStore = new File(gitblit.getBaseFolder(), "ssh-ed25519-hostkey.pem");
+ generateKeyPair(rsaKeyStore, "RSA", 2048);
+ generateKeyPair(ecdsaKeyStore, "ECDSA", 256);
+ generateKeyPair(eddsaKeyStore, "EdDSA", 0);
+ FileKeyPairProvider hostKeyPairProvider = new FileKeyPairProvider();
+ hostKeyPairProvider.setFiles(new String[] {
+ ecdsaKeyStore.getPath(),
+ eddsaKeyStore.getPath(),
+ ed25519KeyStore.getPath(),
+ rsaKeyStore.getPath(),
+ dsaKeyStore.getPath()
+ });
+
+ String sshBackendStr = settings.getString(Keys.git.sshBackend, SshSessionBackend.NIO2.name());
+ SshSessionBackend backend = SshSessionBackend.valueOf(sshBackendStr);
+ if (backend == SshSessionBackend.MINA) {
+ log.warn("MINA SSH backend is no longer supported in the patched image; forcing NIO2");
+ backend = SshSessionBackend.NIO2;
+ sshBackendStr = backend.name();
+ }
+ System.setProperty(IoServiceFactoryFactory.class.getName(), Nio2ServiceFactoryFactory.class.getName());
+
+ 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);
+ }
- List authMethods = settings.getStrings(Keys.git.sshAuthenticationMethods);
- if (authMethods.isEmpty()) {
- authMethods.add(AUTH_PUBLICKEY);
- authMethods.add(AUTH_PASSWORD);
- }
- // Keep backward compatibility with old setting files that use the git.sshWithKrb5 setting.
- if (settings.getBoolean("git.sshWithKrb5", false) && !authMethods.contains(AUTH_GSSAPI)) {
- authMethods.add(AUTH_GSSAPI);
- log.warn("git.sshWithKrb5 is obsolete!");
- log.warn("Please add {} to {} in gitblit.properties!", AUTH_GSSAPI, Keys.git.sshAuthenticationMethods);
- settings.overrideSetting(Keys.git.sshAuthenticationMethods,
- settings.getString(Keys.git.sshAuthenticationMethods, AUTH_PUBLICKEY + " " + AUTH_PASSWORD) + " " + AUTH_GSSAPI);
- }
- if (authMethods.contains(AUTH_PUBLICKEY)) {
- SshKeyAuthenticator keyAuthenticator = new SshKeyAuthenticator(gitblit.getPublicKeyManager(), gitblit);
- sshd.setPublickeyAuthenticator(new CachingPublicKeyAuthenticator(keyAuthenticator));
- log.info("SSH: adding public key authentication method.");
- }
- if (authMethods.contains(AUTH_PASSWORD) || authMethods.contains(AUTH_KBD_INTERACTIVE)) {
- sshd.setPasswordAuthenticator(new UsernamePasswordAuthenticator(gitblit));
- log.info("SSH: adding password authentication method.");
- }
- if (authMethods.contains(AUTH_GSSAPI)) {
- sshd.setGSSAuthenticator(new SshKrbAuthenticator(settings, gitblit));
- log.info("SSH: adding GSSAPI authentication method.");
- }
+ sshd = SshServer.setUpDefaultServer();
+ sshd.setPort(addr.getPort());
+ sshd.setHost(addr.getHostName());
+ sshd.setKeyPairProvider(hostKeyPairProvider);
- sshd.setSessionFactory(new SshServerSessionFactory(sshd));
- sshd.setFileSystemFactory(new DisabledFilesystemFactory());
- sshd.setForwardingFilter(new NonForwardingFilter());
- sshd.setCommandFactory(new SshCommandFactory(gitblit, workQueue));
- sshd.setShellFactory(new WelcomeShell(gitblit));
+ List authMethods = settings.getStrings(Keys.git.sshAuthenticationMethods);
+ if (authMethods.isEmpty()) {
+ authMethods.add(AUTH_PUBLICKEY);
+ authMethods.add(AUTH_PASSWORD);
+ }
+ if (settings.getBoolean("git.sshWithKrb5", false) && !authMethods.contains(AUTH_GSSAPI)) {
+ authMethods.add(AUTH_GSSAPI);
+ log.warn("git.sshWithKrb5 is obsolete!");
+ log.warn("Please add {} to {} in gitblit.properties!", AUTH_GSSAPI, Keys.git.sshAuthenticationMethods);
+ settings.overrideSetting(Keys.git.sshAuthenticationMethods,
+ settings.getString(Keys.git.sshAuthenticationMethods, AUTH_PUBLICKEY + " " + AUTH_PASSWORD) + " " + AUTH_GSSAPI);
+ }
+ if (authMethods.contains(AUTH_PUBLICKEY)) {
+ SshKeyAuthenticator keyAuthenticator = new SshKeyAuthenticator(gitblit.getPublicKeyManager(), gitblit);
+ sshd.setPublickeyAuthenticator(keyAuthenticator);
+ log.info("SSH: adding public key authentication method.");
+ }
+ if (authMethods.contains(AUTH_PASSWORD) || authMethods.contains(AUTH_KBD_INTERACTIVE)) {
+ sshd.setPasswordAuthenticator(new UsernamePasswordAuthenticator(gitblit));
+ log.info("SSH: adding password authentication method.");
+ }
+ if (authMethods.contains(AUTH_GSSAPI)) {
+ sshd.setGSSAuthenticator(new SshKrbAuthenticator(settings, gitblit));
+ log.info("SSH: adding GSSAPI authentication method.");
+ }
- // 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);
+ sshd.setSessionFactory(new SshServerSessionFactory(sshd));
+ sshd.setKexExtensionHandler(new ServerSigAlgsExtensionHandler());
+ sshd.setFileSystemFactory(new DisabledFilesystemFactory());
+ sshd.setForwardingFilter(new NonForwardingFilter());
+ sshd.setCommandFactory(new SshCommandFactory(gitblit, workQueue));
+ sshd.setShellFactory(new WelcomeShell(gitblit));
- run = new AtomicBoolean(false);
- }
+ String version = String.format("%s (%s-%s)", Constants.getGitBlitVersion().replace(' ', '_'),
+ sshd.getVersion(), sshBackendStr);
+ CoreModuleProperties.SERVER_IDENTIFICATION.set(sshd, version);
- public String formatUrl(String gituser, String servername, String repository) {
- IStoredSettings settings = gitblit.getSettings();
+ run = new AtomicBoolean(false);
+ }
- 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);
- }
- }
+ public String formatUrl(String gituser, String servername, String repository) {
+ IStoredSettings settings = gitblit.getSettings();
- /**
- * 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);
- }
+ 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) {
+ return MessageFormat.format("ssh://{0}@{1}/{2}", gituser, displayServername, repository);
+ }
+ return MessageFormat.format("ssh://{0}@{1}:{2,number,0}/{3}",
+ gituser, displayServername, displayPort, repository);
+ }
- sshd.start();
- run.set(true);
+ public synchronized void start() throws IOException {
+ if (run.get()) {
+ throw new IllegalStateException(JGitText.get().daemonAlreadyRunning);
+ }
- String sshBackendStr = gitblit.getSettings().getString(Keys.git.sshBackend,
- SshSessionBackend.NIO2.name());
+ sshd.start();
+ run.set(true);
- log.info(MessageFormat.format(
- "SSH Daemon ({0}) is listening on {1}:{2,number,0}",
- sshBackendStr, sshd.getHost(), sshd.getPort()));
- }
+ 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();
- }
+ public boolean isRunning() {
+ return run.get();
+ }
- /** Stop this daemon. */
- public synchronized void stop() {
- if (run.get()) {
- log.info("SSH Daemon stopping...");
- run.set(false);
+ 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);
- }
- }
- }
+ try {
+ ((SshCommandFactory) sshd.getCommandFactory()).stop();
+ sshd.stop();
+ } catch (IOException e) {
+ log.error("SSH Daemon stop interrupted", e);
+ }
+ }
+ }
static void generateKeyPair(File file, String algorithm, int keySize) {
- if (file.exists()) {
- return;
- }
+ if (file.exists()) {
+ return;
+ }
try {
KeyPairGenerator generator = SecurityUtils.getKeyPairGenerator(algorithm);
if (keySize != 0) {
- generator.initialize(keySize);
- log.info("Generating {}-{} SSH host keypair...", algorithm, keySize);
+ generator.initialize(keySize);
+ log.info("Generating {}-{} SSH host keypair...", algorithm, Integer.valueOf(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);
+ JnaUtils.setFilemode(file, JnaUtils.S_IRUSR | JnaUtils.S_IWUSR);
} catch (UnsatisfiedLinkError | UnsupportedOperationException e) {
- // Unexpected/Unsupported OS or Architecture
+ // Unsupported platform-specific chmod path.
}
- FileOutputStream os = new FileOutputStream(file);
- PemWriter w = new PemWriter(new OutputStreamWriter(os));
- if (algorithm.equals("ED25519")) {
- // This generates a proper OpenSSH formatted ed25519 private key file.
- // It is currently unused because the SSHD library in play doesn't work with proper keys.
- // This is kept in the hope that in the future the library offers proper support.
- AsymmetricKeyParameter keyParam = PrivateKeyFactory.createKey(kp.getPrivate().getEncoded());
- byte[] encKey = OpenSSHPrivateKeyUtil.encodePrivateKey(keyParam);
- w.writeObject(new PemObject("OPENSSH PRIVATE KEY", encKey));
- }
- else if (algorithm.equals("EdDSA")) {
- // This saves the ed25519 key in a file format that the current SSHD library can work with.
- // We call it EDDSA PRIVATE KEY, but that string is given by us and nothing official.
- PrivateKey privateKey = kp.getPrivate();
- if (privateKey instanceof EdDSAPrivateKey) {
- OpenSSHEd25519PrivateKeyEntryDecoder encoder = (OpenSSHEd25519PrivateKeyEntryDecoder)SecurityUtils.getOpenSSHEDDSAPrivateKeyEntryDecoder();
- EdDSAPrivateKey dsaPrivateKey = (EdDSAPrivateKey)privateKey;
- // Jumping through some hoops here, because the decoder expects the key type as a string at the
- // start, but the encoder doesn't put it in. So we have to put it in ourselves.
- ByteArrayOutputStream encos = new ByteArrayOutputStream();
- String type = encoder.encodePrivateKey(encos, dsaPrivateKey);
- ByteArrayOutputStream bos = new ByteArrayOutputStream();
- KeyEntryResolver.encodeString(bos, type);
- encos.writeTo(bos);
- w.writeObject(new PemObject("EDDSA PRIVATE KEY", bos.toByteArray()));
- }
- else {
- log.warn("Unable to encode EdDSA key, got key type " + privateKey.getClass().getCanonicalName());
- }
- }
- else {
- w.writeObject(new JcaMiscPEMGenerator(kp));
- }
- w.flush();
- w.close();
+ try (FileOutputStream os = new FileOutputStream(file)) {
+ try (PemWriter w = new PemWriter(new OutputStreamWriter(os))) {
+ if ("ED25519".equals(algorithm)) {
+ AsymmetricKeyParameter keyParam = PrivateKeyFactory.createKey(kp.getPrivate().getEncoded());
+ byte[] encKey = OpenSSHPrivateKeyUtil.encodePrivateKey(keyParam);
+ w.writeObject(new PemObject("OPENSSH PRIVATE KEY", encKey));
+ } else if ("EdDSA".equals(algorithm)) {
+ PrivateKey privateKey = kp.getPrivate();
+ if (privateKey instanceof EdDSAPrivateKey) {
+ OpenSSHEd25519PrivateKeyEntryDecoder encoder =
+ (OpenSSHEd25519PrivateKeyEntryDecoder) SecurityUtils.getOpenSSHEDDSAPrivateKeyEntryDecoder();
+ EdDSAPrivateKey dsaPrivateKey = (EdDSAPrivateKey) privateKey;
+ EdDSAPublicKey dsaPublicKey = encoder.recoverPublicKey(dsaPrivateKey);
+ SecureByteArrayOutputStream encos = new SecureByteArrayOutputStream();
+ String type = encoder.encodePrivateKey(encos, dsaPrivateKey, dsaPublicKey);
+ SecureByteArrayOutputStream bos = new SecureByteArrayOutputStream();
+ KeyEntryResolver.encodeString(bos, type);
+ encos.writeTo(bos);
+ w.writeObject(new PemObject("EDDSA PRIVATE KEY", bos.toByteArray()));
+ } else {
+ log.warn("Unable to encode EdDSA key, got key type {}", privateKey.getClass().getCanonicalName());
+ }
+ } else {
+ w.writeObject(new JcaMiscPEMGenerator(kp));
+ }
+ }
+ }
} catch (Exception e) {
- log.warn(MessageFormat.format("Unable to generate {0} keypair", algorithm), e);
+ log.error("Unable to generate " + algorithm + " keypair", e);
}
}
}
diff --git a/src/main/java/com/gitblit/transport/ssh/SshDaemonClient.java b/src/main/java/com/gitblit/transport/ssh/SshDaemonClient.java
index 7024a9a99..6bd5967bd 100644
--- a/src/main/java/com/gitblit/transport/ssh/SshDaemonClient.java
+++ b/src/main/java/com/gitblit/transport/ssh/SshDaemonClient.java
@@ -17,58 +17,68 @@
import java.net.SocketAddress;
-import org.apache.sshd.common.AttributeStore.AttributeKey;
+import org.apache.sshd.common.AttributeRepository.AttributeKey;
+import org.apache.sshd.server.session.ServerSession;
import com.gitblit.models.UserModel;
-/**
- *
- * @author Eric Myrhe
- *
- */
public class SshDaemonClient {
- public static final AttributeKey KEY = new AttributeKey();
-
- private final SocketAddress remoteAddress;
-
- private volatile UserModel user;
- private volatile SshKey key;
- private volatile String repositoryName;
-
- SshDaemonClient(SocketAddress peer) {
- this.remoteAddress = peer;
- }
-
- public SocketAddress getRemoteAddress() {
- return remoteAddress;
- }
-
- public UserModel getUser() {
- return user;
- }
-
- public void setUser(UserModel user) {
- this.user = user;
- }
-
- public String getUsername() {
- return user == null ? null : user.username;
- }
-
- public void setRepositoryName(String repositoryName) {
- this.repositoryName = repositoryName;
- }
-
- public String getRepositoryName() {
- return repositoryName;
- }
-
- public SshKey getKey() {
- return key;
- }
-
- public void setKey(SshKey key) {
- this.key = key;
- }
-
+ public static final AttributeKey KEY = new AttributeKey();
+
+ private final SocketAddress remoteAddress;
+
+ private volatile UserModel user;
+ private volatile SshKey key;
+ private volatile String repositoryName;
+
+ SshDaemonClient(SocketAddress peer) {
+ this.remoteAddress = peer;
+ }
+
+ public static SshDaemonClient get(ServerSession session) {
+ SshDaemonClient client = session.getAttribute(KEY);
+ if (client != null) {
+ return client;
+ }
+ synchronized (session) {
+ client = session.getAttribute(KEY);
+ if (client == null) {
+ client = new SshDaemonClient(session.getIoSession().getRemoteAddress());
+ session.setAttribute(KEY, client);
+ }
+ return client;
+ }
+ }
+
+ public SocketAddress getRemoteAddress() {
+ return remoteAddress;
+ }
+
+ public UserModel getUser() {
+ return user;
+ }
+
+ public void setUser(UserModel user) {
+ this.user = user;
+ }
+
+ public String getUsername() {
+ return user == null ? null : user.username;
+ }
+
+ public void setRepositoryName(String repositoryName) {
+ this.repositoryName = repositoryName;
+ }
+
+ public String getRepositoryName() {
+ return repositoryName;
+ }
+
+ public SshKey getKey() {
+ return key;
+ }
+
+ public void setKey(SshKey key) {
+ this.key = key;
+ }
}
diff --git a/src/main/java/com/gitblit/transport/ssh/SshKeyAuthenticator.java b/src/main/java/com/gitblit/transport/ssh/SshKeyAuthenticator.java
index dc9d8a4e5..493962db5 100644
--- a/src/main/java/com/gitblit/transport/ssh/SshKeyAuthenticator.java
+++ b/src/main/java/com/gitblit/transport/ssh/SshKeyAuthenticator.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2014 gitblit.com.
+ * Copyright 2026 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
@@ -19,6 +19,7 @@
import java.util.List;
import java.util.Locale;
+import org.apache.sshd.common.config.keys.KeyUtils;
import org.apache.sshd.server.auth.pubkey.PublickeyAuthenticator;
import org.apache.sshd.server.session.ServerSession;
import org.slf4j.Logger;
@@ -26,52 +27,59 @@
import com.gitblit.manager.IAuthenticationManager;
import com.gitblit.models.UserModel;
-import com.google.common.base.Preconditions;
-/**
- * Authenticates an SSH session against a public key.
- *
- */
public class SshKeyAuthenticator implements PublickeyAuthenticator {
- protected final Logger log = LoggerFactory.getLogger(getClass());
+ protected final Logger log = LoggerFactory.getLogger(getClass());
- protected final IPublicKeyManager keyManager;
+ protected final IPublicKeyManager keyManager;
- protected final IAuthenticationManager authManager;
+ protected final IAuthenticationManager authManager;
- public SshKeyAuthenticator(IPublicKeyManager keyManager, IAuthenticationManager authManager) {
- this.keyManager = keyManager;
- this.authManager = authManager;
- }
+ public SshKeyAuthenticator(IPublicKeyManager keyManager, IAuthenticationManager authManager) {
+ this.keyManager = keyManager;
+ this.authManager = authManager;
+ }
- @Override
- public boolean authenticate(String username, PublicKey suppliedKey, ServerSession session) {
- SshDaemonClient client = session.getAttribute(SshDaemonClient.KEY);
- Preconditions.checkState(client.getUser() == null);
- username = username.toLowerCase(Locale.US);
- List keys = keyManager.getKeys(username);
- if (keys.isEmpty()) {
- log.info("{} has not added any public keys for ssh authentication", username);
- return false;
- }
+ @Override
+ public boolean authenticate(String username, PublicKey suppliedKey, ServerSession session) {
+ SshDaemonClient client = SshDaemonClient.get(session);
+ username = username.toLowerCase(Locale.US);
+ if (client.getUser() != null) {
+ if (username.equals(client.getUsername()) && client.getKey() != null
+ && KeyUtils.compareKeys(client.getKey().getPublicKey(), suppliedKey)) {
+ log.info("ssh publickey auth reused for {}", username);
+ return true;
+ }
+ log.warn("ssh publickey auth state mismatch for {}", username);
+ return false;
+ }
+ log.info("ssh publickey auth attempt for {}", username);
+ List keys = keyManager.getKeys(username);
+ if (keys.isEmpty()) {
+ log.info("{} has not added any public keys for ssh authentication", username);
+ return false;
+ }
- SshKey pk = new SshKey(suppliedKey);
- log.debug("auth supplied {}", pk.getFingerprint());
+ SshKey pk = new SshKey(suppliedKey);
+ log.debug("auth supplied {}", pk.getFingerprint());
- for (SshKey key : keys) {
- log.debug("auth compare to {}", key.getFingerprint());
- if (key.getPublicKey().equals(suppliedKey)) {
- UserModel user = authManager.authenticate(username, key);
- if (user != null) {
- client.setUser(user);
- client.setKey(key);
- return true;
- }
- }
- }
+ for (SshKey key : keys) {
+ log.debug("auth compare to {}", key.getFingerprint());
+ if (KeyUtils.compareKeys(key.getPublicKey(), suppliedKey)) {
+ log.info("ssh publickey auth key match for {}", username);
+ UserModel user = authManager.authenticate(username, key);
+ if (user != null) {
+ client.setUser(user);
+ client.setKey(key);
+ log.info("ssh publickey auth success for {}", username);
+ return true;
+ }
+ log.warn("ssh publickey auth user validation failed for {}", username);
+ }
+ }
- log.warn("could not authenticate {} for SSH using the supplied public key", username);
- return false;
- }
+ log.warn("could not authenticate {} for SSH using the supplied public key", username);
+ return false;
+ }
}
diff --git a/src/main/java/com/gitblit/transport/ssh/SshServerSessionFactory.java b/src/main/java/com/gitblit/transport/ssh/SshServerSessionFactory.java
index fb85781ab..d26b018f5 100644
--- a/src/main/java/com/gitblit/transport/ssh/SshServerSessionFactory.java
+++ b/src/main/java/com/gitblit/transport/ssh/SshServerSessionFactory.java
@@ -17,58 +17,43 @@
import java.net.SocketAddress;
-import org.apache.mina.transport.socket.SocketSessionConfig;
import org.apache.sshd.common.future.CloseFuture;
import org.apache.sshd.common.future.SshFutureListener;
import org.apache.sshd.common.io.IoSession;
-import org.apache.sshd.common.io.mina.MinaSession;
import org.apache.sshd.server.ServerFactoryManager;
import org.apache.sshd.server.session.ServerSessionImpl;
import org.apache.sshd.server.session.SessionFactory;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
-/**
- *
- * @author James Moger
- *
- */
public class SshServerSessionFactory extends SessionFactory {
- private final Logger log = LoggerFactory.getLogger(getClass());
-
- public SshServerSessionFactory(ServerFactoryManager server) {
- super(server);
- }
-
- @Override
- protected ServerSessionImpl createSession(final IoSession io) throws Exception {
- log.info("creating ssh session from {}", io.getRemoteAddress());
-
- if (io instanceof MinaSession) {
- if (((MinaSession) io).getSession().getConfig() instanceof SocketSessionConfig) {
- ((SocketSessionConfig) ((MinaSession) io).getSession().getConfig()).setKeepAlive(true);
- }
- }
-
- final SshServerSession session = (SshServerSession) super.createSession(io);
- SocketAddress peer = io.getRemoteAddress();
- SshDaemonClient client = new SshDaemonClient(peer);
- session.setAttribute(SshDaemonClient.KEY, client);
-
- // TODO(davido): Log a session close without authentication as a
- // failure.
- session.addCloseSessionListener(new SshFutureListener() {
- @Override
- public void operationComplete(CloseFuture future) {
- log.info("closed ssh session from {}", io.getRemoteAddress());
- }
- });
- return session;
- }
-
- @Override
- protected ServerSessionImpl doCreateSession(IoSession ioSession) throws Exception {
- return new SshServerSession(getServer(), ioSession);
- }
+ private final Logger log = LoggerFactory.getLogger(getClass());
+
+ public SshServerSessionFactory(ServerFactoryManager server) {
+ super(server);
+ }
+
+ @Override
+ protected ServerSessionImpl createSession(final IoSession io) throws Exception {
+ log.info("creating ssh session from {}", io.getRemoteAddress());
+
+ final SshServerSession session = (SshServerSession) super.createSession(io);
+ SocketAddress peer = io.getRemoteAddress();
+ SshDaemonClient client = new SshDaemonClient(peer);
+ session.setAttribute(SshDaemonClient.KEY, client);
+
+ session.addCloseSessionListener(new SshFutureListener() {
+ @Override
+ public void operationComplete(CloseFuture future) {
+ log.info("closed ssh session from {}", io.getRemoteAddress());
+ }
+ });
+ return session;
+ }
+
+ @Override
+ protected ServerSessionImpl doCreateSession(IoSession ioSession) throws Exception {
+ return new SshServerSession(getServer(), ioSession);
+ }
}
diff --git a/src/main/java/com/gitblit/transport/ssh/WelcomeShell.java b/src/main/java/com/gitblit/transport/ssh/WelcomeShell.java
index 7ea0f2480..e779854ac 100644
--- a/src/main/java/com/gitblit/transport/ssh/WelcomeShell.java
+++ b/src/main/java/com/gitblit/transport/ssh/WelcomeShell.java
@@ -23,12 +23,13 @@
import java.net.URL;
import java.text.MessageFormat;
-import org.apache.sshd.common.Factory;
-import org.apache.sshd.server.Command;
import org.apache.sshd.server.Environment;
import org.apache.sshd.server.ExitCallback;
-import org.apache.sshd.server.SessionAware;
+import org.apache.sshd.server.channel.ChannelSession;
+import org.apache.sshd.server.command.Command;
import org.apache.sshd.server.session.ServerSession;
+import org.apache.sshd.server.session.ServerSessionAware;
+import org.apache.sshd.server.shell.ShellFactory;
import org.eclipse.jgit.lib.Constants;
import org.eclipse.jgit.util.SystemReader;
@@ -40,191 +41,175 @@
import com.gitblit.transport.ssh.commands.SshCommandFactory;
import com.gitblit.utils.StringUtils;
-/**
- * Class that displays a welcome message for any shell requests.
- *
- */
-public class WelcomeShell implements Factory {
-
- private final IGitblit gitblit;
-
- public WelcomeShell(IGitblit gitblit) {
- this.gitblit = gitblit;
- }
-
- @Override
- public Command create() {
- return new SendMessage(gitblit);
- }
-
- @Override
- public Command get() {
- return create();
- }
-
- private static class SendMessage implements Command, SessionAware {
-
- private final IPublicKeyManager km;
- private final IStoredSettings settings;
- private ServerSession session;
-
- private InputStream in;
- private OutputStream out;
- private OutputStream err;
- private ExitCallback exit;
-
- SendMessage(IGitblit gitblit) {
- this.km = gitblit.getPublicKeyManager();
- this.settings = gitblit.getSettings();
- }
-
- @Override
- public void setInputStream(final InputStream in) {
- this.in = in;
- }
-
- @Override
- public void setOutputStream(final OutputStream out) {
- this.out = out;
- }
-
- @Override
- public void setErrorStream(final OutputStream err) {
- this.err = err;
- }
-
- @Override
- public void setExitCallback(final ExitCallback callback) {
- this.exit = callback;
- }
-
- @Override
- public void setSession(final ServerSession session) {
- this.session = session;
- }
-
- @Override
- public void start(final Environment env) throws IOException {
- err.write(Constants.encode(getMessage()));
- err.flush();
-
- in.close();
- out.close();
- err.close();
- exit.onExit(127);
- }
-
- @Override
- public void destroy() {
- this.session = null;
- }
-
- String getMessage() {
- SshDaemonClient client = session.getAttribute(SshDaemonClient.KEY);
- UserModel user = client.getUser();
- String hostname = getHostname();
- int port = settings.getInteger(Keys.git.sshPort, 0);
- boolean writeKeysIsSupported = true;
- if (km != null) {
- writeKeysIsSupported = km.supportsWritingKeys(user);
- }
-
- final String b1 = StringUtils.rightPad("", 72, '═');
- final String b2 = StringUtils.rightPad("", 72, '─');
- final String nl = "\r\n";
-
- StringBuilder msg = new StringBuilder();
- msg.append(nl);
- msg.append(b1);
- msg.append(nl);
- msg.append(" ");
- msg.append(com.gitblit.Constants.getGitBlitVersion());
- msg.append(nl);
- msg.append(b1);
- msg.append(nl);
- msg.append(nl);
- msg.append(" Hi ");
- msg.append(user.getDisplayName());
- msg.append(", you have successfully connected over SSH.");
- msg.append(nl);
- msg.append(" Interactive shells are not available.");
- msg.append(nl);
- msg.append(nl);
- msg.append(" client: ");
- msg.append(session.getClientVersion());
- msg.append(nl);
- msg.append(nl);
-
- msg.append(b2);
- msg.append(nl);
- msg.append(nl);
- msg.append(" You may clone a repository with the following Git syntax:");
- msg.append(nl);
- msg.append(nl);
-
- msg.append(" git clone ");
- msg.append(formatUrl(hostname, port, user.username));
- msg.append(nl);
- msg.append(nl);
-
- msg.append(b2);
- msg.append(nl);
- msg.append(nl);
-
- if (writeKeysIsSupported && client.getKey() == null) {
- // user has authenticated with a password
- // display add public key instructions
- msg.append(" You may upload an SSH public key with the following syntax:");
- msg.append(nl);
- msg.append(nl);
-
- msg.append(String.format(" cat ~/.ssh/id_rsa.pub | ssh -l %s -p %d %s keys add", user.username, port, hostname));
- msg.append(nl);
- msg.append(nl);
-
- msg.append(b2);
- msg.append(nl);
- msg.append(nl);
- }
-
- // display the core commands
- SshCommandFactory cmdFactory = (SshCommandFactory) session.getFactoryManager().getCommandFactory();
- DispatchCommand root = cmdFactory.createRootDispatcher(client, "");
- String usage = root.usage().replace("\n", nl);
- msg.append(usage);
-
- return msg.toString();
- }
-
- private String getHostname() {
- String host = null;
- String url = settings.getString(Keys.web.canonicalUrl, "https://localhost:8443");
- if (url != null) {
- try {
- host = new URL(url).getHost();
- } catch (MalformedURLException e) {
- }
- }
- if (StringUtils.isEmpty(host)) {
- host = SystemReader.getInstance().getHostname();
- }
- return host;
- }
-
- private String formatUrl(String hostname, int port, String username) {
- int displayPort = settings.getInteger(Keys.git.sshAdvertisedPort, port);
- String displayHostname = settings.getString(Keys.git.sshAdvertisedHost, "");
- if(displayHostname.isEmpty()) {
- displayHostname = hostname;
- }
- if (displayPort == 22) {
- // standard port
- return MessageFormat.format("{0}@{1}/REPOSITORY.git", username, displayHostname);
- } else {
- // non-standard port
- return MessageFormat.format("ssh://{0}@{1}:{2,number,0}/REPOSITORY.git",
- username, displayHostname, displayPort);
- }
- }
- }
+public class WelcomeShell implements ShellFactory {
+
+ private final IGitblit gitblit;
+
+ public WelcomeShell(IGitblit gitblit) {
+ this.gitblit = gitblit;
+ }
+
+ @Override
+ public Command createShell(ChannelSession channel) throws IOException {
+ return new SendMessage(gitblit);
+ }
+
+ private static class SendMessage implements Command, ServerSessionAware {
+
+ private final IPublicKeyManager km;
+ private final IStoredSettings settings;
+ private ServerSession session;
+
+ private InputStream in;
+ private OutputStream out;
+ private OutputStream err;
+ private ExitCallback exit;
+
+ SendMessage(IGitblit gitblit) {
+ this.km = gitblit.getPublicKeyManager();
+ this.settings = gitblit.getSettings();
+ }
+
+ @Override
+ public void setInputStream(InputStream in) {
+ this.in = in;
+ }
+
+ @Override
+ public void setOutputStream(OutputStream out) {
+ this.out = out;
+ }
+
+ @Override
+ public void setErrorStream(OutputStream err) {
+ this.err = err;
+ }
+
+ @Override
+ public void setExitCallback(ExitCallback callback) {
+ this.exit = callback;
+ }
+
+ @Override
+ public void setSession(ServerSession session) {
+ this.session = session;
+ }
+
+ @Override
+ public void start(ChannelSession channel, Environment env) throws IOException {
+ err.write(Constants.encode(getMessage()));
+ err.flush();
+
+ in.close();
+ out.close();
+ err.close();
+ exit.onExit(127);
+ }
+
+ @Override
+ public void destroy(ChannelSession channel) {
+ this.session = null;
+ }
+
+ String getMessage() {
+ SshDaemonClient client = SshDaemonClient.get(session);
+ UserModel user = client.getUser();
+ String hostname = getHostname();
+ int port = settings.getInteger(Keys.git.sshPort, 0);
+ boolean writeKeysIsSupported = true;
+ if (km != null) {
+ writeKeysIsSupported = km.supportsWritingKeys(user);
+ }
+
+ final String b1 = StringUtils.rightPad("", 72, '═');
+ final String b2 = StringUtils.rightPad("", 72, '─');
+ final String nl = "\r\n";
+
+ StringBuilder msg = new StringBuilder();
+ msg.append(nl);
+ msg.append(b1);
+ msg.append(nl);
+ msg.append(" ");
+ msg.append(com.gitblit.Constants.getGitBlitVersion());
+ msg.append(nl);
+ msg.append(b1);
+ msg.append(nl);
+ msg.append(nl);
+ msg.append(" Hi ");
+ msg.append(user.getDisplayName());
+ msg.append(", you have successfully connected over SSH.");
+ msg.append(nl);
+ msg.append(" Interactive shells are not available.");
+ msg.append(nl);
+ msg.append(nl);
+ msg.append(" client: ");
+ msg.append(session.getClientVersion());
+ msg.append(nl);
+ msg.append(nl);
+
+ msg.append(b2);
+ msg.append(nl);
+ msg.append(nl);
+ msg.append(" You may clone a repository with the following Git syntax:");
+ msg.append(nl);
+ msg.append(nl);
+
+ msg.append(" git clone ");
+ msg.append(formatUrl(hostname, port, user.username));
+ msg.append(nl);
+ msg.append(nl);
+
+ msg.append(b2);
+ msg.append(nl);
+ msg.append(nl);
+
+ if (writeKeysIsSupported && client.getKey() == null) {
+ msg.append(" You may upload an SSH public key with the following syntax:");
+ msg.append(nl);
+ msg.append(nl);
+
+ msg.append(String.format(" cat ~/.ssh/id_rsa.pub | ssh -l %s -p %d %s keys add", user.username, port, hostname));
+ msg.append(nl);
+ msg.append(nl);
+
+ msg.append(b2);
+ msg.append(nl);
+ msg.append(nl);
+ }
+
+ SshCommandFactory cmdFactory = (SshCommandFactory) session.getFactoryManager().getCommandFactory();
+ DispatchCommand root = cmdFactory.createRootDispatcher(client, "");
+ String usage = root.usage().replace("\n", nl);
+ msg.append(usage);
+
+ return msg.toString();
+ }
+
+ private String getHostname() {
+ String host = null;
+ String url = settings.getString(Keys.web.canonicalUrl, "https://localhost:8443");
+ if (url != null) {
+ try {
+ host = new URL(url).getHost();
+ } catch (MalformedURLException e) {
+ }
+ }
+ if (StringUtils.isEmpty(host)) {
+ host = SystemReader.getInstance().getHostname();
+ }
+ return host;
+ }
+
+ private String formatUrl(String hostname, int port, String username) {
+ int displayPort = settings.getInteger(Keys.git.sshAdvertisedPort, port);
+ String displayHostname = settings.getString(Keys.git.sshAdvertisedHost, "");
+ if (displayHostname.isEmpty()) {
+ displayHostname = hostname;
+ }
+ if (displayPort == 22) {
+ return MessageFormat.format("ssh://{0}@{1}/{2}", username, displayHostname, "${repository}");
+ }
+ return MessageFormat.format("ssh://{0}@{1}:{2,number,0}/{3}", username, displayHostname, displayPort, "${repository}");
+ }
+ }
}
diff --git a/src/main/java/com/gitblit/transport/ssh/commands/BaseCommand.java b/src/main/java/com/gitblit/transport/ssh/commands/BaseCommand.java
index ab2756d02..a953b3d20 100644
--- a/src/main/java/com/gitblit/transport/ssh/commands/BaseCommand.java
+++ b/src/main/java/com/gitblit/transport/ssh/commands/BaseCommand.java
@@ -28,11 +28,12 @@
import java.util.concurrent.atomic.AtomicReference;
import org.apache.sshd.common.SshException;
-import org.apache.sshd.server.Command;
import org.apache.sshd.server.Environment;
import org.apache.sshd.server.ExitCallback;
-import org.apache.sshd.server.SessionAware;
+import org.apache.sshd.server.channel.ChannelSession;
+import org.apache.sshd.server.command.Command;
import org.apache.sshd.server.session.ServerSession;
+import org.apache.sshd.server.session.ServerSessionAware;
import org.kohsuke.args4j.Argument;
import org.kohsuke.args4j.CmdLineException;
import org.kohsuke.args4j.Option;
@@ -47,522 +48,380 @@
import com.google.common.base.Charsets;
import com.google.common.util.concurrent.Atomics;
-public abstract class BaseCommand implements Command, SessionAware {
-
- private static final Logger log = LoggerFactory.getLogger(BaseCommand.class);
-
- private static final int PRIVATE_STATUS = 1 << 30;
-
- public final static int STATUS_CANCEL = PRIVATE_STATUS | 1;
-
- public final static int STATUS_NOT_FOUND = PRIVATE_STATUS | 2;
-
- public final static int STATUS_NOT_ADMIN = PRIVATE_STATUS | 3;
-
- protected InputStream in;
-
- protected OutputStream out;
-
- protected OutputStream err;
-
- protected ExitCallback exit;
-
- protected ServerSession session;
-
- /** Ssh command context */
- private SshCommandContext ctx;
-
- /** Text of the command line which lead up to invoking this instance. */
- private String commandName = "";
-
- /** Unparsed command line options. */
- private String[] argv;
-
- /** The task, as scheduled on a worker thread. */
- private final AtomicReference> task;
-
- private WorkQueue workQueue;
-
- public BaseCommand() {
- task = Atomics.newReference();
- }
-
- @Override
- public void setSession(final ServerSession session) {
- this.session = session;
- }
-
- @Override
- public void destroy() {
- log.debug("destroying " + getClass().getName());
- Future> future = task.getAndSet(null);
- if (future != null && !future.isDone()) {
- future.cancel(true);
- }
- session = null;
- ctx = null;
- }
-
- protected static PrintWriter toPrintWriter(final OutputStream o) {
- return new PrintWriter(new BufferedWriter(new OutputStreamWriter(o, Charsets.UTF_8)));
- }
-
- @Override
- public abstract void start(Environment env) throws IOException;
-
- protected void provideStateTo(final BaseCommand cmd) {
- cmd.setContext(ctx);
- cmd.setWorkQueue(workQueue);
- cmd.setInputStream(in);
- cmd.setOutputStream(out);
- cmd.setErrorStream(err);
- cmd.setExitCallback(exit);
- }
-
- public WorkQueue getWorkQueue() {
- return workQueue;
- }
-
- public void setWorkQueue(WorkQueue workQueue) {
- this.workQueue = workQueue;
- }
-
- public void setContext(SshCommandContext ctx) {
- this.ctx = ctx;
- }
-
- public SshCommandContext getContext() {
- return ctx;
- }
-
- @Override
- public void setInputStream(final InputStream in) {
- this.in = in;
- }
-
- @Override
- public void setOutputStream(final OutputStream out) {
- this.out = out;
- }
-
- @Override
- public void setErrorStream(final OutputStream err) {
- this.err = err;
- }
-
- @Override
- public void setExitCallback(final ExitCallback callback) {
- this.exit = callback;
- }
-
- protected String getName() {
- return commandName;
- }
-
- void setName(final String prefix) {
- this.commandName = prefix;
- }
-
- public String[] getArguments() {
- return argv;
- }
-
- public void setArguments(final String[] argv) {
- this.argv = argv;
- }
-
- /**
- * Parses the command line argument, injecting parsed values into fields.
- *
- * This method must be explicitly invoked to cause a parse.
- *
- * @throws UnloggedFailure
- * if the command line arguments were invalid.
- * @see Option
- * @see Argument
- */
- protected void parseCommandLine() throws UnloggedFailure {
- parseCommandLine(this);
- }
-
- /**
- * Parses the command line argument, injecting parsed values into fields.
- *
- * This method must be explicitly invoked to cause a parse.
- *
- * @param options
- * object whose fields declare Option and Argument annotations to
- * describe the parameters of the command. Usually {@code this}.
- * @throws UnloggedFailure
- * if the command line arguments were invalid.
- * @see Option
- * @see Argument
- */
- protected void parseCommandLine(Object options) throws UnloggedFailure {
- final CmdLineParser clp = newCmdLineParser(options);
- try {
- clp.parseArgument(argv);
- } catch (IllegalArgumentException err) {
- if (!clp.wasHelpRequestedByOption()) {
- throw new UnloggedFailure(1, "fatal: " + err.getMessage());
- }
- } catch (CmdLineException err) {
- if (!clp.wasHelpRequestedByOption()) {
- throw new UnloggedFailure(1, "fatal: " + err.getMessage());
- }
- }
-
- if (clp.wasHelpRequestedByOption()) {
- CommandMetaData meta = getClass().getAnnotation(CommandMetaData.class);
- String title = meta.name().toUpperCase() + ": " + meta.description();
- String b = com.gitblit.utils.StringUtils.leftPad("", title.length() + 2, '═');
- StringWriter msg = new StringWriter();
- msg.write('\n');
- msg.write(b);
- msg.write('\n');
- msg.write(' ');
- msg.write(title);
- msg.write('\n');
- msg.write(b);
- msg.write("\n\n");
- msg.write("USAGE\n");
- msg.write("─────\n");
- msg.write(' ');
- msg.write(commandName);
- msg.write('\n');
- msg.write(" ");
- clp.printSingleLineUsage(msg, null);
- msg.write("\n\n");
- String txt = getUsageText();
- if (!StringUtils.isEmpty(txt)) {
- msg.write(txt);
- msg.write("\n\n");
- }
- msg.write("ARGUMENTS & OPTIONS\n");
- msg.write("───────────────────\n");
- clp.printUsage(msg, null);
- msg.write('\n');
- String examples = usage().trim();
- if (!StringUtils.isEmpty(examples)) {
- msg.write('\n');
- msg.write("EXAMPLES\n");
- msg.write("────────\n");
- msg.write(examples);
- msg.write('\n');
- }
-
- throw new UnloggedFailure(1, msg.toString());
- }
- }
-
- /** Construct a new parser for this command's received command line. */
- protected CmdLineParser newCmdLineParser(Object options) {
- return new CmdLineParser(options);
- }
-
- public String usage() {
- Class extends BaseCommand> clazz = getClass();
- if (clazz.isAnnotationPresent(UsageExamples.class)) {
- return examples(clazz.getAnnotation(UsageExamples.class).examples());
- } else if (clazz.isAnnotationPresent(UsageExample.class)) {
- return examples(clazz.getAnnotation(UsageExample.class));
- }
- return "";
- }
-
- protected String getUsageText() {
- return "";
- }
-
- protected String examples(UsageExample... examples) {
- int sshPort = getContext().getGitblit().getSettings().getInteger(Keys.git.sshPort, 29418);
- String username = getContext().getClient().getUsername();
- String hostname = "localhost";
- String ssh = String.format("ssh -l %s -p %d %s", username, sshPort, hostname);
-
- StringBuilder sb = new StringBuilder();
- for (UsageExample example : examples) {
- sb.append(example.description()).append("\n\n");
- String syntax = example.syntax();
- syntax = syntax.replace("${ssh}", ssh);
- syntax = syntax.replace("${username}", username);
- syntax = syntax.replace("${cmd}", commandName);
- sb.append(" ").append(syntax).append("\n\n");
- }
- return sb.toString();
- }
-
- protected void showHelp() throws UnloggedFailure {
- argv = new String [] { "--help" };
- parseCommandLine();
- }
-
- private final class TaskThunk implements CancelableRunnable {
- private final CommandRunnable thunk;
- private final String taskName;
-
- private TaskThunk(final CommandRunnable thunk) {
- this.thunk = thunk;
-
- StringBuilder m = new StringBuilder();
- m.append(ctx.getCommandLine());
- this.taskName = m.toString();
- }
-
- @Override
- public void cancel() {
- synchronized (this) {
- try {
- onExit(STATUS_CANCEL);
- } finally {
- ctx = null;
- }
- }
- }
-
- @Override
- public void run() {
- synchronized (this) {
- final Thread thisThread = Thread.currentThread();
- final String thisName = thisThread.getName();
- int rc = 0;
- try {
- thisThread.setName("SSH " + taskName);
- thunk.run();
-
- out.flush();
- err.flush();
- } catch (Throwable e) {
- try {
- out.flush();
- } catch (Throwable e2) {
- }
- try {
- err.flush();
- } catch (Throwable e2) {
- }
- rc = handleError(e);
- } finally {
- try {
- onExit(rc);
- } finally {
- thisThread.setName(thisName);
- }
- }
- }
- }
-
- @Override
- public String toString() {
- return taskName;
- }
- }
-
- /** Runnable function which can throw an exception. */
- public interface CommandRunnable {
- void run() throws Exception;
- }
-
- /** Runnable function which can retrieve a project name related to the task */
- public interface RepositoryCommandRunnable extends CommandRunnable {
- String getRepository();
- }
-
- /**
- * Spawn a function into its own thread.
- *
- * Typically this should be invoked within
- * {@link Command#start(Environment)}, such as:
- *
- *
- * startThread(new Runnable() {
- * public void run() {
- * runImp();
- * }
- * });
- *
- *
- * @param thunk
- * the runnable to execute on the thread, performing the
- * command's logic.
- */
- protected void startThread(final Runnable thunk) {
- startThread(new CommandRunnable() {
- @Override
- public void run() throws Exception {
- thunk.run();
- }
- });
- }
-
- /**
- * Terminate this command and return a result code to the remote client.
- *
- * Commands should invoke this at most once.
- *
- * @param rc exit code for the remote client.
- */
- protected void onExit(final int rc) {
- exit.onExit(rc);
- }
-
- private int handleError(final Throwable e) {
- if ((e.getClass() == IOException.class && "Pipe closed".equals(e.getMessage())) ||
- (e.getClass() == SshException.class && "Already closed".equals(e.getMessage())) ||
- e.getClass() == InterruptedIOException.class) {
- // This is sshd telling us the client just dropped off while
- // we were waiting for a read or a write to complete. Either
- // way its not really a fatal error. Don't log it.
- //
- return 127;
- }
-
- if (e instanceof UnloggedFailure) {
- } else {
- final StringBuilder m = new StringBuilder();
- m.append("Internal server error");
- String user = ctx.getClient().getUsername();
- if (user != null) {
- m.append(" (user ");
- m.append(user);
- m.append(")");
- }
- m.append(" during ");
- m.append(ctx.getCommandLine());
- log.error(m.toString(), e);
- }
-
- if (e instanceof Failure) {
- final Failure f = (Failure) e;
- try {
- err.write((f.getMessage() + "\n").getBytes(Charsets.UTF_8));
- err.flush();
- } catch (IOException e2) {
- } catch (Throwable e2) {
- log.warn("Cannot send failure message to client", e2);
- }
- return f.exitCode;
-
- } else {
- try {
- err.write("fatal: internal server error\n".getBytes(Charsets.UTF_8));
- err.flush();
- } catch (IOException e2) {
- } catch (Throwable e2) {
- log.warn("Cannot send internal server error message to client", e2);
- }
- return 128;
- }
- }
-
- /**
- * Spawn a function into its own thread.
- *
- * Typically this should be invoked within
- * {@link Command#start(Environment)}, such as:
- *
- *
- * startThread(new CommandRunnable() {
- * public void run() throws Exception {
- * runImp();
- * }
- * });
- *
- *
- * If the function throws an exception, it is translated to a simple message
- * for the client, a non-zero exit code, and the stack trace is logged.
- *
- * @param thunk
- * the runnable to execute on the thread, performing the
- * command's logic.
- */
- protected void startThread(final CommandRunnable thunk) {
- final TaskThunk tt = new TaskThunk(thunk);
- task.set(workQueue.getDefaultQueue().submit(tt));
- }
-
- /** Thrown from {@link CommandRunnable#run()} with client message and code. */
- public static class Failure extends Exception {
- private static final long serialVersionUID = 1L;
-
- final int exitCode;
-
- /**
- * Create a new failure.
- *
- * @param exitCode
- * exit code to return the client, which indicates the
- * failure status of this command. Should be between 1 and
- * 255, inclusive.
- * @param msg
- * message to also send to the client's stderr.
- */
- public Failure(final int exitCode, final String msg) {
- this(exitCode, msg, null);
- }
-
- /**
- * Create a new failure.
- *
- * @param exitCode
- * exit code to return the client, which indicates the
- * failure status of this command. Should be between 1 and
- * 255, inclusive.
- * @param msg
- * message to also send to the client's stderr.
- * @param why
- * stack trace to include in the server's log, but is not
- * sent to the client's stderr.
- */
- public Failure(final int exitCode, final String msg, final Throwable why) {
- super(msg, why);
- this.exitCode = exitCode;
- }
- }
-
- /** Thrown from {@link CommandRunnable#run()} with client message and code. */
- public static class UnloggedFailure extends Failure {
- private static final long serialVersionUID = 1L;
-
- /**
- * Create a new failure.
- *
- * @param msg
- * message to also send to the client's stderr.
- */
- public UnloggedFailure(final String msg) {
- this(1, msg);
- }
-
- /**
- * Create a new failure.
- *
- * @param exitCode
- * exit code to return the client, which indicates the
- * failure status of this command. Should be between 1 and
- * 255, inclusive.
- * @param msg
- * message to also send to the client's stderr.
- */
- public UnloggedFailure(final int exitCode, final String msg) {
- this(exitCode, msg, null);
- }
-
- /**
- * Create a new failure.
- *
- * @param exitCode
- * exit code to return the client, which indicates the
- * failure status of this command. Should be between 1 and
- * 255, inclusive.
- * @param msg
- * message to also send to the client's stderr.
- * @param why
- * stack trace to include in the server's log, but is not
- * sent to the client's stderr.
- */
- public UnloggedFailure(final int exitCode, final String msg, final Throwable why) {
- super(exitCode, msg, why);
- }
- }
+public abstract class BaseCommand implements Command, ServerSessionAware {
+
+ private static final Logger log = LoggerFactory.getLogger(BaseCommand.class);
+ private static final int PRIVATE_STATUS = 1 << 30;
+
+ public static final int STATUS_CANCEL = PRIVATE_STATUS | 1;
+ public static final int STATUS_NOT_FOUND = PRIVATE_STATUS | 2;
+ public static final int STATUS_NOT_ADMIN = PRIVATE_STATUS | 3;
+
+ protected InputStream in;
+ protected OutputStream out;
+ protected OutputStream err;
+ protected ExitCallback exit;
+ protected ServerSession session;
+
+ private SshCommandContext ctx;
+ private String commandName = "";
+ private String[] argv;
+ private final AtomicReference> task;
+ private WorkQueue workQueue;
+
+ public BaseCommand() {
+ task = Atomics.newReference();
+ }
+
+ @Override
+ public void setSession(ServerSession session) {
+ this.session = session;
+ }
+
+ public void destroy() {
+ log.debug("destroying " + getClass().getName());
+ Future> future = task.getAndSet(null);
+ if (future != null && !future.isDone()) {
+ future.cancel(true);
+ }
+ session = null;
+ ctx = null;
+ }
+
+ @Override
+ public final void destroy(ChannelSession channel) {
+ destroy();
+ }
+
+ protected static PrintWriter toPrintWriter(OutputStream o) {
+ return new PrintWriter(new BufferedWriter(new OutputStreamWriter(o, Charsets.UTF_8)));
+ }
+
+ @Override
+ public final void start(ChannelSession channel, Environment env) throws IOException {
+ start(env);
+ }
+
+ public abstract void start(Environment env) throws IOException;
+
+ protected void provideStateTo(BaseCommand cmd) {
+ cmd.setContext(ctx);
+ cmd.setWorkQueue(workQueue);
+ cmd.setInputStream(in);
+ cmd.setOutputStream(out);
+ cmd.setErrorStream(err);
+ cmd.setExitCallback(exit);
+ }
+
+ public WorkQueue getWorkQueue() {
+ return workQueue;
+ }
+
+ public void setWorkQueue(WorkQueue workQueue) {
+ this.workQueue = workQueue;
+ }
+
+ public void setContext(SshCommandContext ctx) {
+ this.ctx = ctx;
+ }
+
+ public SshCommandContext getContext() {
+ return ctx;
+ }
+
+ @Override
+ public void setInputStream(InputStream in) {
+ this.in = in;
+ }
+
+ @Override
+ public void setOutputStream(OutputStream out) {
+ this.out = out;
+ }
+
+ @Override
+ public void setErrorStream(OutputStream err) {
+ this.err = err;
+ }
+
+ @Override
+ public void setExitCallback(ExitCallback callback) {
+ this.exit = callback;
+ }
+
+ protected String getName() {
+ return commandName;
+ }
+
+ void setName(String prefix) {
+ this.commandName = prefix;
+ }
+
+ public String[] getArguments() {
+ return argv;
+ }
+
+ public void setArguments(String[] argv) {
+ this.argv = argv;
+ }
+
+ protected void parseCommandLine() throws UnloggedFailure {
+ parseCommandLine(this);
+ }
+
+ protected void parseCommandLine(Object options) throws UnloggedFailure {
+ final CmdLineParser clp = newCmdLineParser(options);
+ try {
+ clp.parseArgument(argv);
+ } catch (IllegalArgumentException err) {
+ if (!clp.wasHelpRequestedByOption()) {
+ throw new UnloggedFailure(1, "fatal: " + err.getMessage());
+ }
+ } catch (CmdLineException err) {
+ if (!clp.wasHelpRequestedByOption()) {
+ throw new UnloggedFailure(1, "fatal: " + err.getMessage());
+ }
+ }
+
+ if (clp.wasHelpRequestedByOption()) {
+ CommandMetaData meta = getClass().getAnnotation(CommandMetaData.class);
+ String title = meta.name().toUpperCase() + ": " + meta.description();
+ String b = com.gitblit.utils.StringUtils.leftPad("", title.length() + 2, '═');
+ StringWriter msg = new StringWriter();
+ msg.write('\n');
+ msg.write(b);
+ msg.write('\n');
+ msg.write(' ');
+ msg.write(title);
+ msg.write('\n');
+ msg.write(b);
+ msg.write("\n\n");
+ msg.write("USAGE\n");
+ msg.write("─────\n");
+ msg.write(' ');
+ msg.write(commandName);
+ msg.write('\n');
+ msg.write(" ");
+ clp.printSingleLineUsage(msg, null);
+ msg.write("\n\n");
+ String txt = getUsageText();
+ if (!StringUtils.isEmpty(txt)) {
+ msg.write(txt);
+ msg.write("\n\n");
+ }
+ msg.write("ARGUMENTS & OPTIONS\n");
+ msg.write("───────────────────\n");
+ clp.printUsage(msg, null);
+ msg.write('\n');
+ String examples = usage().trim();
+ if (!StringUtils.isEmpty(examples)) {
+ msg.write('\n');
+ msg.write("EXAMPLES\n");
+ msg.write("────────\n");
+ msg.write(examples);
+ msg.write('\n');
+ }
+
+ throw new UnloggedFailure(1, msg.toString());
+ }
+ }
+
+ protected CmdLineParser newCmdLineParser(Object options) {
+ return new CmdLineParser(options);
+ }
+
+ public String usage() {
+ Class extends BaseCommand> clazz = getClass();
+ if (clazz.isAnnotationPresent(UsageExamples.class)) {
+ return examples(clazz.getAnnotation(UsageExamples.class).examples());
+ } else if (clazz.isAnnotationPresent(UsageExample.class)) {
+ return examples(clazz.getAnnotation(UsageExample.class));
+ }
+ return "";
+ }
+
+ protected String getUsageText() {
+ return "";
+ }
+
+ protected String examples(UsageExample... examples) {
+ int sshPort = getContext().getGitblit().getSettings().getInteger(Keys.git.sshPort, 29418);
+ String username = getContext().getClient().getUsername();
+ String hostname = "localhost";
+ String ssh = String.format("ssh -l %s -p %d %s", username, sshPort, hostname);
+
+ StringBuilder sb = new StringBuilder();
+ for (UsageExample example : examples) {
+ sb.append(example.description()).append("\n\n");
+ String syntax = example.syntax();
+ syntax = syntax.replace("${ssh}", ssh);
+ syntax = syntax.replace("${username}", username);
+ syntax = syntax.replace("${cmd}", commandName);
+ sb.append(" ").append(syntax).append("\n\n");
+ }
+ return sb.toString();
+ }
+
+ protected void showHelp() throws UnloggedFailure {
+ argv = new String[] { "--help" };
+ parseCommandLine();
+ }
+
+ private final class TaskThunk implements CancelableRunnable {
+ private final CommandRunnable thunk;
+ private final String taskName;
+
+ private TaskThunk(CommandRunnable thunk) {
+ this.thunk = thunk;
+ this.taskName = ctx.getCommandLine();
+ }
+
+ @Override
+ public void cancel() {
+ synchronized (this) {
+ try {
+ onExit(STATUS_CANCEL);
+ } finally {
+ ctx = null;
+ }
+ }
+ }
+
+ @Override
+ public void run() {
+ synchronized (this) {
+ final Thread thisThread = Thread.currentThread();
+ final String thisName = thisThread.getName();
+ int rc = 0;
+ try {
+ thisThread.setName("SSH " + taskName);
+ thunk.run();
+
+ out.flush();
+ err.flush();
+ } catch (Throwable e) {
+ try {
+ out.flush();
+ } catch (Throwable e2) {
+ }
+ try {
+ err.flush();
+ } catch (Throwable e2) {
+ }
+ rc = handleError(e);
+ } finally {
+ try {
+ onExit(rc);
+ } finally {
+ thisThread.setName(thisName);
+ }
+ }
+ }
+ }
+
+ @Override
+ public String toString() {
+ return taskName;
+ }
+ }
+
+ public interface CommandRunnable {
+ void run() throws Exception;
+ }
+
+ public interface RepositoryCommandRunnable extends CommandRunnable {
+ String getRepository();
+ }
+
+ protected void startThread(final Runnable thunk) {
+ startThread(new CommandRunnable() {
+ @Override
+ public void run() throws Exception {
+ thunk.run();
+ }
+ });
+ }
+
+ protected void onExit(final int rc) {
+ exit.onExit(rc);
+ }
+
+ private int handleError(final Throwable e) {
+ if ((e.getClass() == IOException.class && "Pipe closed".equals(e.getMessage()))
+ || (e.getClass() == SshException.class && "Already closed".equals(e.getMessage()))
+ || e.getClass() == InterruptedIOException.class) {
+ return 127;
+ }
+
+ if (!(e instanceof UnloggedFailure)) {
+ final StringBuilder m = new StringBuilder();
+ m.append("Internal server error");
+ String user = ctx.getClient().getUsername();
+ if (user != null) {
+ m.append(" (user ");
+ m.append(user);
+ m.append(")");
+ }
+ m.append(" during ");
+ m.append(ctx.getCommandLine());
+ log.error(m.toString(), e);
+ }
+
+ if (e instanceof Failure) {
+ final Failure f = (Failure) e;
+ try {
+ err.write((f.getMessage() + "\n").getBytes(Charsets.UTF_8));
+ err.flush();
+ } catch (IOException e2) {
+ } catch (Throwable e2) {
+ log.warn("Cannot send failure message to client", e2);
+ }
+ return f.exitCode;
+ }
+
+ try {
+ err.write("fatal: internal server error\n".getBytes(Charsets.UTF_8));
+ err.flush();
+ } catch (IOException e2) {
+ } catch (Throwable e2) {
+ log.warn("Cannot send internal server error message to client", e2);
+ }
+ return 128;
+ }
+
+ protected void startThread(final CommandRunnable thunk) {
+ final TaskThunk tt = new TaskThunk(thunk);
+ task.set(workQueue.getDefaultQueue().submit(tt));
+ }
+
+ public static class Failure extends Exception {
+ private static final long serialVersionUID = 1L;
+
+ final int exitCode;
+
+ public Failure(final int exitCode, final String msg) {
+ this(exitCode, msg, null);
+ }
+
+ public Failure(final int exitCode, final String msg, final Throwable why) {
+ super(msg, why);
+ this.exitCode = exitCode;
+ }
+ }
+
+ public static class UnloggedFailure extends Failure {
+ private static final long serialVersionUID = 1L;
+
+ public UnloggedFailure(final String msg) {
+ this(1, msg);
+ }
+
+ public UnloggedFailure(final int exitCode, final String msg) {
+ this(exitCode, msg, null);
+ }
+
+ public UnloggedFailure(final int exitCode, final String msg, final Throwable why) {
+ super(exitCode, msg, why);
+ }
+ }
}
diff --git a/src/main/java/com/gitblit/transport/ssh/commands/DispatchCommand.java b/src/main/java/com/gitblit/transport/ssh/commands/DispatchCommand.java
index d17a4ebf1..e9cf01afd 100644
--- a/src/main/java/com/gitblit/transport/ssh/commands/DispatchCommand.java
+++ b/src/main/java/com/gitblit/transport/ssh/commands/DispatchCommand.java
@@ -26,7 +26,6 @@
import java.util.Set;
import java.util.TreeSet;
-import org.apache.sshd.server.Command;
import org.apache.sshd.server.Environment;
import org.kohsuke.args4j.Argument;
import org.slf4j.Logger;
@@ -42,385 +41,258 @@
import com.google.common.base.Strings;
import com.google.common.collect.Maps;
-/**
- * Parses an SSH command-line and dispatches the command to the appropriate
- * BaseCommand instance.
- *
- * @since 1.5.0
- */
public abstract class DispatchCommand extends BaseCommand implements ExtensionPoint {
- private Logger log = LoggerFactory.getLogger(getClass());
-
- @Argument(index = 0, required = false, metaVar = "COMMAND", handler = SubcommandHandler.class)
- private String commandName;
-
- @Argument(index = 1, multiValued = true, metaVar = "ARG")
- private List args = new ArrayList();
-
- private final Set> commands;
- private final Map dispatchers;
- private final Map aliasToCommand;
- private final Map> commandToAliases;
- private final List instantiated;
- private Map> map;
-
- protected DispatchCommand() {
- commands = new HashSet>();
- dispatchers = Maps.newHashMap();
- aliasToCommand = Maps.newHashMap();
- commandToAliases = Maps.newHashMap();
- instantiated = new ArrayList();
- }
-
- @Override
- public void destroy() {
- super.destroy();
- commands.clear();
- aliasToCommand.clear();
- commandToAliases.clear();
- map = null;
-
- for (BaseCommand command : instantiated) {
- command.destroy();
- }
- instantiated.clear();
-
- for (DispatchCommand dispatcher : dispatchers.values()) {
- dispatcher.destroy();
- }
- dispatchers.clear();
- }
-
- /**
- * Setup this dispatcher. Commands and nested dispatchers are normally
- * registered within this method.
- *
- * @since 1.5.0
- */
- protected abstract void setup();
-
- /**
- * Register a command or a dispatcher by it's class.
- *
- * @param clazz
- */
- @SuppressWarnings("unchecked")
- protected final void register(Class extends BaseCommand> clazz) {
- if (DispatchCommand.class.isAssignableFrom(clazz)) {
- registerDispatcher((Class extends DispatchCommand>) clazz);
- return;
- }
-
- registerCommand(clazz);
- }
-
- /**
- * Register a command or a dispatcher instance.
- *
- * @param cmd
- */
- protected final void register(BaseCommand cmd) {
- if (cmd instanceof DispatchCommand) {
- registerDispatcher((DispatchCommand) cmd);
- return;
- }
- registerCommand(cmd);
- }
-
- private void registerDispatcher(Class extends DispatchCommand> clazz) {
- try {
- DispatchCommand dispatcher = clazz.newInstance();
- registerDispatcher(dispatcher);
- } catch (Exception e) {
- log.error("failed to instantiate {}", clazz.getName());
- }
- }
-
- private void registerDispatcher(DispatchCommand dispatcher) {
- Class extends DispatchCommand> dispatcherClass = dispatcher.getClass();
- if (!dispatcherClass.isAnnotationPresent(CommandMetaData.class)) {
- throw new RuntimeException(MessageFormat.format("{0} must be annotated with {1}!", dispatcher.getName(),
- CommandMetaData.class.getName()));
+ private Logger log = LoggerFactory.getLogger(getClass());
+
+ @Argument(index = 0, required = false, metaVar = "COMMAND", handler = SubcommandHandler.class)
+ private String commandName;
+
+ @Argument(index = 1, multiValued = true, metaVar = "ARG")
+ private List args = new ArrayList();
+
+ private final Set> commands;
+ private final Map dispatchers;
+ private final Map aliasToCommand;
+ private final Map> commandToAliases;
+ private final List instantiated;
+ private Map> map;
+
+ protected DispatchCommand() {
+ commands = new HashSet>();
+ dispatchers = Maps.newHashMap();
+ aliasToCommand = Maps.newHashMap();
+ commandToAliases = Maps.newHashMap();
+ instantiated = new ArrayList();
+ }
+
+ @Override
+ public void destroy() {
+ super.destroy();
+ commands.clear();
+ aliasToCommand.clear();
+ commandToAliases.clear();
+ map = null;
+
+ for (BaseCommand command : instantiated) {
+ command.destroy();
+ }
+ instantiated.clear();
+
+ for (DispatchCommand dispatcher : dispatchers.values()) {
+ dispatcher.destroy();
+ }
+ dispatchers.clear();
+ }
+
+ protected abstract void setup();
+
+ @SuppressWarnings("unchecked")
+ protected final void register(Class extends BaseCommand> clazz) {
+ if (DispatchCommand.class.isAssignableFrom(clazz)) {
+ registerDispatcher((Class extends DispatchCommand>) clazz);
+ return;
+ }
+
+ registerCommand(clazz);
+ }
+
+ protected final void register(BaseCommand cmd) {
+ if (cmd instanceof DispatchCommand) {
+ registerDispatcher((DispatchCommand) cmd);
+ return;
+ }
+ registerCommand(cmd);
+ }
+
+ private void registerDispatcher(Class extends DispatchCommand> clazz) {
+ try {
+ DispatchCommand dispatcher = clazz.newInstance();
+ registerDispatcher(dispatcher);
+ } catch (Exception e) {
+ log.error("failed to instantiate {}", clazz.getName());
+ }
+ }
+
+ private void registerDispatcher(DispatchCommand dispatcher) {
+ Class extends DispatchCommand> dispatcherClass = dispatcher.getClass();
+ if (!dispatcherClass.isAnnotationPresent(CommandMetaData.class)) {
+ throw new RuntimeException(MessageFormat.format("{0} must be annotated with {1}!", dispatcher.getName(),
+ CommandMetaData.class.getName()));
+ }
+
+ UserModel user = getContext().getClient().getUser();
+ CommandMetaData meta = dispatcherClass.getAnnotation(CommandMetaData.class);
+ if (meta.admin() && !user.canAdmin()) {
+ log.debug(MessageFormat.format("excluding admin dispatcher {0} for {1}", meta.name(), user.username));
+ return;
+ }
+
+ try {
+ dispatcher.setContext(getContext());
+ dispatcher.setWorkQueue(getWorkQueue());
+ dispatcher.setup();
+ if (dispatcher.commands.isEmpty() && dispatcher.dispatchers.isEmpty()) {
+ log.debug(MessageFormat.format("excluding empty dispatcher {0} for {1}", meta.name(), user.username));
+ return;
+ }
+
+ log.debug("registering {} dispatcher", meta.name());
+ dispatchers.put(meta.name(), dispatcher);
+ for (String alias : meta.aliases()) {
+ aliasToCommand.put(alias, meta.name());
+ if (!commandToAliases.containsKey(meta.name())) {
+ commandToAliases.put(meta.name(), new ArrayList());
+ }
+ commandToAliases.get(meta.name()).add(alias);
+ }
+ } catch (Exception e) {
+ log.error("failed to register {} dispatcher", meta.name());
+ }
+ }
+
+ private void registerCommand(Class extends BaseCommand> clazz) {
+ if (!clazz.isAnnotationPresent(CommandMetaData.class)) {
+ throw new RuntimeException(MessageFormat.format("{0} must be annotated with {1}!", clazz.getName(),
+ CommandMetaData.class.getName()));
+ }
+
+ UserModel user = getContext().getClient().getUser();
+ CommandMetaData meta = clazz.getAnnotation(CommandMetaData.class);
+ if (meta.admin() && !user.canAdmin()) {
+ log.debug(MessageFormat.format("excluding admin command {0} for {1}", meta.name(), user.username));
+ return;
+ }
+ commands.add(clazz);
+ }
+
+ private void registerCommand(BaseCommand cmd) {
+ if (!cmd.getClass().isAnnotationPresent(CommandMetaData.class)) {
+ throw new RuntimeException(MessageFormat.format("{0} must be annotated with {1}!", cmd.getName(),
+ CommandMetaData.class.getName()));
+ }
+
+ UserModel user = getContext().getClient().getUser();
+ CommandMetaData meta = cmd.getClass().getAnnotation(CommandMetaData.class);
+ if (meta.admin() && !user.canAdmin()) {
+ log.debug(MessageFormat.format("excluding admin command {0} for {1}", meta.name(), user.username));
+ return;
+ }
+ commands.add(cmd.getClass());
+ instantiated.add(cmd);
+ }
+
+ private Map> getMap() {
+ if (map == null) {
+ map = Maps.newHashMapWithExpectedSize(commands.size());
+ for (Class extends BaseCommand> cmd : commands) {
+ CommandMetaData meta = cmd.getAnnotation(CommandMetaData.class);
+ if (map.containsKey(meta.name()) || aliasToCommand.containsKey(meta.name())) {
+ log.warn("{} already contains the \"{}\" command!", getName(), meta.name());
+ } else {
+ map.put(meta.name(), cmd);
+ }
+ for (String alias : meta.aliases()) {
+ if (map.containsKey(alias) || aliasToCommand.containsKey(alias)) {
+ log.warn("{} already contains the \"{}\" command!", getName(), alias);
+ } else {
+ aliasToCommand.put(alias, meta.name());
+ if (!commandToAliases.containsKey(meta.name())) {
+ commandToAliases.put(meta.name(), new ArrayList());
+ }
+ commandToAliases.get(meta.name()).add(alias);
+ }
+ }
+ }
+
+ for (Map.Entry entry : dispatchers.entrySet()) {
+ map.put(entry.getKey(), entry.getValue().getClass());
+ }
+ }
+ return map;
+ }
+
+ @Override
+ public void start(Environment env) throws IOException {
+ try {
+ parseCommandLine();
+ if (Strings.isNullOrEmpty(commandName)) {
+ StringWriter msg = new StringWriter();
+ msg.write(usage());
+ throw new UnloggedFailure(1, msg.toString());
+ }
+
+ BaseCommand cmd = getCommand();
+ if (getName().isEmpty()) {
+ cmd.setName(commandName);
+ } else {
+ cmd.setName(getName() + " " + commandName);
+ }
+ cmd.setArguments(args.toArray(new String[args.size()]));
+
+ provideStateTo(cmd);
+ cmd.start(env);
+
+ } catch (UnloggedFailure e) {
+ String msg = e.getMessage();
+ if (!msg.endsWith("\n")) {
+ msg += "\n";
+ }
+ err.write(msg.getBytes(Charsets.UTF_8));
+ err.flush();
+ exit.onExit(e.exitCode);
+ }
+ }
+
+ protected BaseCommand getCommand() throws UnloggedFailure {
+ Map> commandMap = getMap();
+ String cmd = commandName;
+ if (aliasToCommand.containsKey(cmd)) {
+ cmd = aliasToCommand.get(cmd);
}
- UserModel user = getContext().getClient().getUser();
- CommandMetaData meta = dispatcherClass.getAnnotation(CommandMetaData.class);
- if (meta.admin() && !user.canAdmin()) {
- log.debug(MessageFormat.format("excluding admin dispatcher {0} for {1}",
- meta.name(), user.username));
- return;
+ Class extends BaseCommand> clazz = commandMap.get(cmd);
+ if (clazz == null) {
+ throw new UnloggedFailure(STATUS_NOT_FOUND, "fatal: '" + commandName + "' is not a registered command");
}
- try {
- dispatcher.setContext(getContext());
- dispatcher.setWorkQueue(getWorkQueue());
- dispatcher.setup();
- if (dispatcher.commands.isEmpty() && dispatcher.dispatchers.isEmpty()) {
- log.debug(MessageFormat.format("excluding empty dispatcher {0} for {1}",
- meta.name(), user.username));
- return;
- }
-
- log.debug("registering {} dispatcher", meta.name());
- dispatchers.put(meta.name(), dispatcher);
- for (String alias : meta.aliases()) {
- aliasToCommand.put(alias, meta.name());
- if (!commandToAliases.containsKey(meta.name())) {
- commandToAliases.put(meta.name(), new ArrayList());
- }
- commandToAliases.get(meta.name()).add(alias);
- }
- } catch (Exception e) {
- log.error("failed to register {} dispatcher", meta.name());
- }
- }
-
- /**
- * Registers a command as long as the user is permitted to execute it.
- *
- * @param clazz
- */
- private void registerCommand(Class extends BaseCommand> clazz) {
- if (!clazz.isAnnotationPresent(CommandMetaData.class)) {
- throw new RuntimeException(MessageFormat.format("{0} must be annotated with {1}!", clazz.getName(),
- CommandMetaData.class.getName()));
- }
-
- UserModel user = getContext().getClient().getUser();
- CommandMetaData meta = clazz.getAnnotation(CommandMetaData.class);
- if (meta.admin() && !user.canAdmin()) {
- log.debug(MessageFormat.format("excluding admin command {0} for {1}", meta.name(), user.username));
- return;
- }
- commands.add(clazz);
- }
-
- /**
- * Registers a command as long as the user is permitted to execute it.
- *
- * @param cmd
- */
- private void registerCommand(BaseCommand cmd) {
- if (!cmd.getClass().isAnnotationPresent(CommandMetaData.class)) {
- throw new RuntimeException(MessageFormat.format("{0} must be annotated with {1}!", cmd.getName(),
- CommandMetaData.class.getName()));
- }
-
- UserModel user = getContext().getClient().getUser();
- CommandMetaData meta = cmd.getClass().getAnnotation(CommandMetaData.class);
- if (meta.admin() && !user.canAdmin()) {
- log.debug(MessageFormat.format("excluding admin command {0} for {1}", meta.name(), user.username));
- return;
- }
- commands.add(cmd.getClass());
- instantiated.add(cmd);
- }
-
- private Map> getMap() {
- if (map == null) {
- map = Maps.newHashMapWithExpectedSize(commands.size());
- for (Class extends BaseCommand> cmd : commands) {
- CommandMetaData meta = cmd.getAnnotation(CommandMetaData.class);
- if (map.containsKey(meta.name()) || aliasToCommand.containsKey(meta.name())) {
- log.warn("{} already contains the \"{}\" command!", getName(), meta.name());
- } else {
- map.put(meta.name(), cmd);
- }
- for (String alias : meta.aliases()) {
- if (map.containsKey(alias) || aliasToCommand.containsKey(alias)) {
- log.warn("{} already contains the \"{}\" command!", getName(), alias);
- } else {
- aliasToCommand.put(alias, meta.name());
- if (!commandToAliases.containsKey(meta.name())) {
- commandToAliases.put(meta.name(), new ArrayList());
- }
- commandToAliases.get(meta.name()).add(alias);
- }
- }
- }
-
- for (Map.Entry entry : dispatchers.entrySet()) {
- map.put(entry.getKey(), entry.getValue().getClass());
- }
- }
- return map;
- }
-
- @Override
- public void start(Environment env) throws IOException {
- try {
- parseCommandLine();
- if (Strings.isNullOrEmpty(commandName)) {
- StringWriter msg = new StringWriter();
- msg.write(usage());
- throw new UnloggedFailure(1, msg.toString());
- }
-
- BaseCommand cmd = getCommand();
- if (getName().isEmpty()) {
- cmd.setName(commandName);
- } else {
- cmd.setName(getName() + " " + commandName);
- }
- cmd.setArguments(args.toArray(new String[args.size()]));
-
- provideStateTo(cmd);
- // atomicCmd.set(cmd);
- cmd.start(env);
-
- } catch (UnloggedFailure e) {
- String msg = e.getMessage();
- if (!msg.endsWith("\n")) {
- msg += "\n";
- }
- err.write(msg.getBytes(Charsets.UTF_8));
- err.flush();
- exit.onExit(e.exitCode);
- }
- }
-
- private BaseCommand getCommand() throws UnloggedFailure {
- Map> map = getMap();
- String name = commandName;
- if (aliasToCommand.containsKey(commandName)) {
- name = aliasToCommand.get(name);
- }
- if (dispatchers.containsKey(name)) {
- return dispatchers.get(name);
- }
- final Class extends BaseCommand> c = map.get(name);
- if (c == null) {
- String msg = (getName().isEmpty() ? "Gitblit" : getName()) + ": " + commandName + ": not found";
- throw new UnloggedFailure(1, msg);
- }
-
- for (BaseCommand cmd : instantiated) {
- // use an already instantiated command
- if (cmd.getClass().equals(c)) {
- return cmd;
- }
- }
-
- BaseCommand cmd = null;
- try {
- cmd = c.newInstance();
- instantiated.add(cmd);
- } catch (Exception e) {
- throw new UnloggedFailure(1, MessageFormat.format("Failed to instantiate {0} command", commandName));
- }
- return cmd;
- }
-
- private boolean hasVisibleCommands() {
- boolean visible = false;
- for (Class extends BaseCommand> cmd : commands) {
- visible |= !cmd.getAnnotation(CommandMetaData.class).hidden();
- if (visible) {
- return true;
- }
- }
- for (DispatchCommand cmd : dispatchers.values()) {
- visible |= cmd.hasVisibleCommands();
- if (visible) {
- return true;
- }
- }
- return false;
- }
-
- public String getDescription() {
- return getClass().getAnnotation(CommandMetaData.class).description();
- }
-
- @Override
- public String usage() {
- Set cmds = new TreeSet();
- Set dcs = new TreeSet();
- Map displayNames = Maps.newHashMap();
- int maxLength = -1;
- Map> m = getMap();
- for (String name : m.keySet()) {
- Class extends BaseCommand> c = m.get(name);
- CommandMetaData meta = c.getAnnotation(CommandMetaData.class);
- if (meta.hidden()) {
- continue;
- }
-
- String displayName = name + (meta.admin() ? "*" : "");
- if (commandToAliases.containsKey(meta.name())) {
- displayName = name + (meta.admin() ? "*" : "")+ " (" + Joiner.on(',').join(commandToAliases.get(meta.name())) + ")";
- }
- displayNames.put(name, displayName);
-
- maxLength = Math.max(maxLength, displayName.length());
- if (DispatchCommand.class.isAssignableFrom(c)) {
- DispatchCommand d = dispatchers.get(name);
- if (d.hasVisibleCommands()) {
- dcs.add(name);
- }
- } else {
- cmds.add(name);
- }
- }
- String format = "%-" + maxLength + "s %s";
-
- final StringBuilder usage = new StringBuilder();
- if (!StringUtils.isEmpty(getName())) {
- String title = getName().toUpperCase() + ": " + getDescription();
- String b = com.gitblit.utils.StringUtils.leftPad("", title.length() + 2, '═');
- usage.append('\n');
- usage.append(b).append('\n');
- usage.append(' ').append(title).append('\n');
- usage.append(b).append('\n');
- usage.append('\n');
- }
-
- if (!cmds.isEmpty()) {
- usage.append("Available commands");
- if (!getName().isEmpty()) {
- usage.append(" of ");
- usage.append(getName());
- }
- usage.append(" are:\n");
- usage.append("\n");
- for (String name : cmds) {
- final Class extends Command> c = m.get(name);
- String displayName = displayNames.get(name);
- CommandMetaData meta = c.getAnnotation(CommandMetaData.class);
- usage.append(" ");
- usage.append(String.format(format, displayName, Strings.nullToEmpty(meta.description())));
- usage.append("\n");
- }
- usage.append("\n");
- }
-
- if (!dcs.isEmpty()) {
- usage.append("Available command dispatchers");
- if (!getName().isEmpty()) {
- usage.append(" of ");
- usage.append(getName());
- }
- usage.append(" are:\n");
- usage.append("\n");
- for (String name : dcs) {
- final Class extends BaseCommand> c = m.get(name);
- String displayName = displayNames.get(name);
- CommandMetaData meta = c.getAnnotation(CommandMetaData.class);
- usage.append(" ");
- usage.append(String.format(format, displayName, Strings.nullToEmpty(meta.description())));
- usage.append("\n");
- }
- usage.append("\n");
- }
-
- usage.append("See '");
- if (!StringUtils.isEmpty(getName())) {
- usage.append(getName());
- usage.append(' ');
- }
- usage.append("COMMAND --help' for more information.\n");
- usage.append("\n");
- return usage.toString();
- }
+ BaseCommand command = dispatchers.get(cmd);
+ if (command == null) {
+ try {
+ command = clazz.newInstance();
+ instantiated.add(command);
+ } catch (Exception e) {
+ throw new UnloggedFailure(1, "fatal: failed to instantiate command");
+ }
+ }
+
+ return command;
+ }
+
+ @Override
+ protected String getUsageText() {
+ if (getMap().isEmpty()) {
+ return "";
+ }
+
+ final StringBuilder sb = new StringBuilder();
+ sb.append("COMMANDS\n");
+ sb.append("────────\n");
+
+ TreeSet names = new TreeSet(getMap().keySet());
+ for (String name : names) {
+ if (aliasToCommand.containsKey(name)) {
+ continue;
+ }
+ Class extends BaseCommand> cmd = getMap().get(name);
+ CommandMetaData meta = cmd.getAnnotation(CommandMetaData.class);
+ List aliases = commandToAliases.get(name);
+ sb.append(" ").append(name);
+ if (aliases != null && !aliases.isEmpty()) {
+ sb.append(" (").append(Joiner.on(", ").join(aliases)).append(")");
+ }
+ sb.append("\n ").append(meta.description()).append("\n\n");
+ }
+ return sb.toString().trim();
+ }
}
diff --git a/src/main/java/com/gitblit/transport/ssh/commands/SshCommandFactory.java b/src/main/java/com/gitblit/transport/ssh/commands/SshCommandFactory.java
index fa4b91673..53340cf9b 100644
--- a/src/main/java/com/gitblit/transport/ssh/commands/SshCommandFactory.java
+++ b/src/main/java/com/gitblit/transport/ssh/commands/SshCommandFactory.java
@@ -28,12 +28,13 @@
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicReference;
-import org.apache.sshd.server.Command;
-import org.apache.sshd.server.CommandFactory;
import org.apache.sshd.server.Environment;
import org.apache.sshd.server.ExitCallback;
-import org.apache.sshd.server.SessionAware;
+import org.apache.sshd.server.channel.ChannelSession;
+import org.apache.sshd.server.command.Command;
+import org.apache.sshd.server.command.CommandFactory;
import org.apache.sshd.server.session.ServerSession;
+import org.apache.sshd.server.session.ServerSessionAware;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -45,229 +46,221 @@
import com.google.common.util.concurrent.ThreadFactoryBuilder;
public class SshCommandFactory implements CommandFactory {
- private static final Logger logger = LoggerFactory.getLogger(SshCommandFactory.class);
+ private static final Logger logger = LoggerFactory.getLogger(SshCommandFactory.class);
- private final WorkQueue workQueue;
- private final IGitblit gitblit;
- private final ScheduledExecutorService startExecutor;
- private final ExecutorService destroyExecutor;
+ private final WorkQueue workQueue;
+ private final IGitblit gitblit;
+ private final ScheduledExecutorService startExecutor;
+ private final ExecutorService destroyExecutor;
- public SshCommandFactory(IGitblit gitblit, WorkQueue workQueue) {
- this.gitblit = gitblit;
- this.workQueue = workQueue;
+ public SshCommandFactory(IGitblit gitblit, WorkQueue workQueue) {
+ this.gitblit = gitblit;
+ this.workQueue = workQueue;
- int threads = gitblit.getSettings().getInteger(Keys.git.sshCommandStartThreads, 2);
- startExecutor = workQueue.createQueue(threads, "SshCommandStart");
- destroyExecutor = Executors.newSingleThreadExecutor(
- new ThreadFactoryBuilder()
- .setNameFormat("SshCommandDestroy-%s")
- .setDaemon(true)
- .build());
- }
+ int threads = gitblit.getSettings().getInteger(Keys.git.sshCommandStartThreads, 2);
+ startExecutor = workQueue.createQueue(threads, "SshCommandStart");
+ destroyExecutor = Executors.newSingleThreadExecutor(new ThreadFactoryBuilder()
+ .setNameFormat("SshCommandDestroy-%s")
+ .setDaemon(true)
+ .build());
+ }
- public void stop() {
- destroyExecutor.shutdownNow();
- }
+ public void stop() {
+ destroyExecutor.shutdownNow();
+ }
- public RootDispatcher createRootDispatcher(SshDaemonClient client, String commandLine) {
- return new RootDispatcher(gitblit, client, commandLine, workQueue);
- }
+ public RootDispatcher createRootDispatcher(SshDaemonClient client, String commandLine) {
+ return new RootDispatcher(gitblit, client, commandLine, workQueue);
+ }
- @Override
- public Command createCommand(final String commandLine) {
- return new Trampoline(commandLine);
- }
+ @Override
+ public Command createCommand(ChannelSession channel, String commandLine) throws IOException {
+ return new Trampoline(commandLine);
+ }
- private class Trampoline implements Command, SessionAware {
- private final String[] argv;
- private ServerSession session;
- private InputStream in;
- private OutputStream out;
- private OutputStream err;
- private ExitCallback exit;
- private Environment env;
- private String cmdLine;
- private DispatchCommand cmd;
- private final AtomicBoolean logged;
- private final AtomicReference> task;
+ private class Trampoline implements Command, ServerSessionAware {
+ private final String[] argv;
+ private ServerSession session;
+ private InputStream in;
+ private OutputStream out;
+ private OutputStream err;
+ private ExitCallback exit;
+ private Environment env;
+ private String cmdLine;
+ private DispatchCommand cmd;
+ private final AtomicBoolean logged;
+ private final AtomicReference> task;
- Trampoline(String line) {
- if (line.startsWith("git-")) {
- line = "git " + line;
- }
- cmdLine = line;
- argv = split(line);
- logged = new AtomicBoolean();
- task = Atomics.newReference();
- }
+ Trampoline(String line) {
+ if (line.startsWith("git-")) {
+ line = "git " + line;
+ }
+ cmdLine = line;
+ argv = split(line);
+ logged = new AtomicBoolean();
+ task = Atomics.newReference();
+ }
- @Override
- public void setSession(ServerSession session) {
- this.session = session;
- }
+ @Override
+ public void setSession(ServerSession session) {
+ this.session = session;
+ }
- @Override
- public void setInputStream(final InputStream in) {
- this.in = in;
- }
+ @Override
+ public void setInputStream(InputStream in) {
+ this.in = in;
+ }
- @Override
- public void setOutputStream(final OutputStream out) {
- this.out = out;
- }
+ @Override
+ public void setOutputStream(OutputStream out) {
+ this.out = out;
+ }
- @Override
- public void setErrorStream(final OutputStream err) {
- this.err = err;
- }
+ @Override
+ public void setErrorStream(OutputStream err) {
+ this.err = err;
+ }
- @Override
- public void setExitCallback(final ExitCallback callback) {
- this.exit = callback;
- }
+ @Override
+ public void setExitCallback(ExitCallback callback) {
+ this.exit = callback;
+ }
- @Override
- public void start(final Environment env) throws IOException {
- this.env = env;
- task.set(startExecutor.submit(new Runnable() {
- @Override
- public void run() {
- try {
- onStart();
- } catch (Exception e) {
- logger.warn("Cannot start command ", e);
- }
- }
+ @Override
+ public void start(ChannelSession channel, Environment env) throws IOException {
+ this.env = env;
+ task.set(startExecutor.submit(new Runnable() {
+ @Override
+ public void run() {
+ try {
+ onStart();
+ } catch (Exception e) {
+ logger.warn("Cannot start command ", e);
+ }
+ }
- @Override
- public String toString() {
- return "start (user " + session.getUsername() + ")";
- }
- }));
- }
+ @Override
+ public String toString() {
+ return "start (user " + session.getUsername() + ")";
+ }
+ }));
+ }
- private void onStart() throws IOException {
- synchronized (this) {
- SshDaemonClient client = session.getAttribute(SshDaemonClient.KEY);
- try {
- cmd = createRootDispatcher(client, cmdLine);
- cmd.setArguments(argv);
- cmd.setInputStream(in);
- cmd.setOutputStream(out);
- cmd.setErrorStream(err);
- cmd.setExitCallback(new ExitCallback() {
- @Override
- public void onExit(int rc, String exitMessage) {
- exit.onExit(translateExit(rc), exitMessage);
- log(rc);
- }
+ private void onStart() throws IOException {
+ synchronized (this) {
+ try {
+ final SshDaemonClient client = SshDaemonClient.get(session);
+ cmd = createRootDispatcher(client, cmdLine);
+ cmd.setArguments(argv);
+ cmd.setInputStream(in);
+ cmd.setOutputStream(out);
+ cmd.setErrorStream(err);
+ cmd.setExitCallback(new ExitCallback() {
+ @Override
+ public void onExit(int rc, String exitMessage, boolean closeImmediately) {
+ exit.onExit(translateExit(rc), exitMessage, closeImmediately);
+ log(rc);
+ }
+ });
+ cmd.start(env);
+ } catch (RuntimeException e) {
+ throw e;
+ }
+ }
+ }
- @Override
- public void onExit(int rc) {
- exit.onExit(translateExit(rc));
- log(rc);
- }
- });
- cmd.start(env);
- } finally {
- client = null;
- }
- }
- }
+ private int translateExit(final int rc) {
+ switch (rc) {
+ case BaseCommand.STATUS_NOT_ADMIN:
+ return 1;
+ case BaseCommand.STATUS_CANCEL:
+ return 15;
+ case BaseCommand.STATUS_NOT_FOUND:
+ return 127;
+ default:
+ return rc;
+ }
+ }
- private int translateExit(final int rc) {
- switch (rc) {
- case BaseCommand.STATUS_NOT_ADMIN:
- return 1;
+ private void log(final int rc) {
+ if (logged.compareAndSet(false, true)) {
+ logger.info("onExecute: {} exits with: {}", cmd.getClass().getSimpleName(), rc);
+ }
+ }
- case BaseCommand.STATUS_CANCEL:
- return 15 /* SIGKILL */;
+ @Override
+ public void destroy(ChannelSession channel) {
+ Future> future = task.getAndSet(null);
+ if (future != null) {
+ future.cancel(true);
+ destroyExecutor.execute(new Runnable() {
+ @Override
+ public void run() {
+ onDestroy();
+ }
+ });
+ }
+ }
- case BaseCommand.STATUS_NOT_FOUND:
- return 127 /* POSIX not found */;
+ private void onDestroy() {
+ synchronized (this) {
+ if (cmd != null) {
+ try {
+ cmd.destroy();
+ } finally {
+ cmd = null;
+ }
+ }
+ }
+ }
+ }
- default:
- return rc;
- }
- }
-
- private void log(final int rc) {
- if (logged.compareAndSet(false, true)) {
- logger.info("onExecute: {} exits with: {}", cmd.getClass().getSimpleName(), rc);
- }
- }
-
- @Override
- public void destroy() {
- Future> future = task.getAndSet(null);
- if (future != null) {
- future.cancel(true);
- destroyExecutor.execute(new Runnable() {
- @Override
- public void run() {
- onDestroy();
- }
- });
- }
- }
-
- private void onDestroy() {
- synchronized (this) {
- if (cmd != null) {
- try {
- cmd.destroy();
- } finally {
- cmd = null;
- }
- }
- }
- }
- }
-
- /** Split a command line into a string array. */
- static public String[] split(String commandLine) {
- final List list = new ArrayList();
- boolean inquote = false;
- boolean inDblQuote = false;
- StringBuilder r = new StringBuilder();
- for (int ip = 0; ip < commandLine.length();) {
- final char b = commandLine.charAt(ip++);
- switch (b) {
- case '\t':
- case ' ':
- if (inquote || inDblQuote)
- r.append(b);
- else if (r.length() > 0) {
- list.add(r.toString());
- r = new StringBuilder();
- }
- continue;
- case '\"':
- if (inquote)
- r.append(b);
- else
- inDblQuote = !inDblQuote;
- continue;
- case '\'':
- if (inDblQuote)
- r.append(b);
- else
- inquote = !inquote;
- continue;
- case '\\':
- if (inquote || ip == commandLine.length())
- r.append(b); // literal within a quote
- else
- r.append(commandLine.charAt(ip++));
- continue;
- default:
- r.append(b);
- continue;
- }
- }
- if (r.length() > 0) {
- list.add(r.toString());
- }
- return list.toArray(new String[list.size()]);
- }
+ public static String[] split(String commandLine) {
+ final List list = new ArrayList();
+ boolean inquote = false;
+ boolean inDblQuote = false;
+ StringBuilder r = new StringBuilder();
+ for (int ip = 0; ip < commandLine.length();) {
+ final char b = commandLine.charAt(ip++);
+ switch (b) {
+ case '\t':
+ case ' ':
+ if (inquote || inDblQuote) {
+ r.append(b);
+ } else if (r.length() > 0) {
+ list.add(r.toString());
+ r = new StringBuilder();
+ }
+ continue;
+ case '"':
+ if (inquote) {
+ r.append(b);
+ } else {
+ inDblQuote = !inDblQuote;
+ }
+ continue;
+ case '\'':
+ if (inDblQuote) {
+ r.append(b);
+ } else {
+ inquote = !inquote;
+ }
+ continue;
+ case '\\':
+ if (inquote || ip == commandLine.length()) {
+ r.append(b);
+ } else {
+ r.append(commandLine.charAt(ip++));
+ }
+ continue;
+ default:
+ r.append(b);
+ continue;
+ }
+ }
+ if (r.length() > 0) {
+ list.add(r.toString());
+ }
+ return list.toArray(new String[list.size()]);
+ }
}
diff --git a/src/test/java/com/gitblit/tests/GitBlitSuite.java b/src/test/java/com/gitblit/tests/GitBlitSuite.java
index e0328112a..698b066cc 100644
--- a/src/test/java/com/gitblit/tests/GitBlitSuite.java
+++ b/src/test/java/com/gitblit/tests/GitBlitSuite.java
@@ -17,12 +17,14 @@
import java.io.File;
import java.io.FileInputStream;
-import java.io.FileOutputStream;
-import java.io.IOException;
-import java.lang.reflect.Field;
-import java.util.concurrent.Executors;
-import java.util.concurrent.atomic.AtomicBoolean;
-import java.util.concurrent.atomic.AtomicInteger;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.lang.reflect.Field;
+import java.net.InetSocketAddress;
+import java.net.Socket;
+import java.util.concurrent.Executors;
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.concurrent.atomic.AtomicInteger;
import java.util.zip.ZipEntry;
import java.util.zip.ZipInputStream;
@@ -110,10 +112,11 @@ public class GitBlitSuite {
public static String gitServletUrl = "http://localhost:" + port + "/git";
public static String gitDaemonUrl = "git://localhost:" + gitPort;
public static String sshDaemonUrl = "ssh://admin@localhost:" + sshPort;
- public static String account = "admin";
- public static String password = "admin";
-
- private static AtomicBoolean started = new AtomicBoolean(false);
+ public static String account = "admin";
+ public static String password = "admin";
+
+ private static AtomicBoolean started = new AtomicBoolean(false);
+ private static AtomicBoolean repositoriesInitialized = new AtomicBoolean(false);
public static Repository getHelloworldRepository() {
return getRepository("helloworld.git");
@@ -151,13 +154,13 @@ private static Repository getRepository(String name) {
return null;
}
- public static boolean startGitblit() throws Exception {
- if (started.get()) {
- // already started
- return false;
- }
-
- GitServletTest.deleteWorkingFolders();
+ public static boolean startGitblit() throws Exception {
+ if (started.get()) {
+ ensureTestRepositories();
+ return false;
+ }
+
+ GitServletTest.deleteWorkingFolders();
// Start a Gitblit instance
Executors.newSingleThreadExecutor().execute(new Runnable() {
@@ -174,22 +177,24 @@ public void run() {
"--settings", GitBlitSuite.SETTINGS.getAbsolutePath(),
"--baseFolder", "data");
}
- });
-
- // Wait a few seconds for it to be running
- Thread.sleep(5000);
-
- started.set(true);
- return true;
- }
-
- public static void stopGitblit() throws Exception {
- // Stop Gitblit
- GitBlitServer.main("--stop", "--shutdownPort", "" + shutdownPort);
-
- // Wait a few seconds for it to be running
- Thread.sleep(5000);
- }
+ });
+
+ waitForPort("http", port);
+ waitForPort("ssh", sshPort);
+
+ started.set(true);
+ ensureTestRepositories();
+ return true;
+ }
+
+ public static void stopGitblit() throws Exception {
+ // Stop Gitblit
+ GitBlitServer.main("--stop", "--shutdownPort", "" + shutdownPort);
+
+ // Wait a few seconds for it to be running
+ Thread.sleep(5000);
+ started.set(false);
+ }
public static void deleteRefChecksFolder() throws IOException {
File refChecks = new File(GitBlitSuite.REPOSITORIES, "refchecks");
@@ -198,46 +203,20 @@ public static void deleteRefChecksFolder() throws IOException {
}
}
- @BeforeClass
- public static void setUp() throws Exception {
- //"refchecks" folder is used in GitServletTest;
- //need be deleted before Gitblit server instance is started
- deleteRefChecksFolder();
- startGitblit();
-
- if (REPOSITORIES.exists() || REPOSITORIES.mkdirs()) {
- if (!HELLOWORLD_REPO_SOURCE.exists()) {
- unzipRepository(HELLOWORLD_REPO_SOURCE.getPath() + ".zip", HELLOWORLD_REPO_SOURCE.getParentFile());
- }
- if (!TICGIT_REPO_SOURCE.exists()) {
- unzipRepository(TICGIT_REPO_SOURCE.getPath() + ".zip", TICGIT_REPO_SOURCE.getParentFile());
- }
- if (!AMBITION_REPO_SOURCE.exists()) {
- unzipRepository(AMBITION_REPO_SOURCE.getPath() + ".zip", AMBITION_REPO_SOURCE.getParentFile());
- }
- if (!GITECTIVE_REPO_SOURCE.exists()) {
- unzipRepository(GITECTIVE_REPO_SOURCE.getPath() + ".zip", GITECTIVE_REPO_SOURCE.getParentFile());
- }
- cloneOrFetch("helloworld.git", HELLOWORLD_REPO_SOURCE.getAbsolutePath());
- cloneOrFetch("ticgit.git", TICGIT_REPO_SOURCE.getAbsolutePath());
- cloneOrFetch("test/jgit.git", "https://github.com/eclipse-jgit/jgit.git");
- cloneOrFetch("test/helloworld.git", HELLOWORLD_REPO_SOURCE.getAbsolutePath());
- cloneOrFetch("test/ambition.git", AMBITION_REPO_SOURCE.getAbsolutePath());
- cloneOrFetch("test/gitective.git", GITECTIVE_REPO_SOURCE.getAbsolutePath());
-
- showRemoteBranches("ticgit.git");
- automaticallyTagBranchTips("ticgit.git");
- showRemoteBranches("test/jgit.git");
- automaticallyTagBranchTips("test/jgit.git");
- }
- }
+ @BeforeClass
+ public static void setUp() throws Exception {
+ //"refchecks" folder is used in GitServletTest;
+ //need be deleted before Gitblit server instance is started
+ deleteRefChecksFolder();
+ startGitblit();
+ }
@AfterClass
public static void tearDown() throws Exception {
stopGitblit();
}
- private static void cloneOrFetch(String name, String fromUrl) throws Exception {
+ private static void cloneOrFetch(String name, String fromUrl) throws Exception {
System.out.print("Fetching " + name + "... ");
try {
JGitUtils.cloneRepository(REPOSITORIES, name, fromUrl);
@@ -245,7 +224,57 @@ private static void cloneOrFetch(String name, String fromUrl) throws Exception {
System.out.println("Error: " + t.getMessage());
}
System.out.println("done.");
- }
+ }
+
+ private static void ensureTestRepositories() throws Exception {
+ if (repositoriesInitialized.get()) {
+ return;
+ }
+ synchronized (repositoriesInitialized) {
+ if (repositoriesInitialized.get()) {
+ return;
+ }
+ if (REPOSITORIES.exists() || REPOSITORIES.mkdirs()) {
+ if (!HELLOWORLD_REPO_SOURCE.exists()) {
+ unzipRepository(HELLOWORLD_REPO_SOURCE.getPath() + ".zip", HELLOWORLD_REPO_SOURCE.getParentFile());
+ }
+ if (!TICGIT_REPO_SOURCE.exists()) {
+ unzipRepository(TICGIT_REPO_SOURCE.getPath() + ".zip", TICGIT_REPO_SOURCE.getParentFile());
+ }
+ if (!AMBITION_REPO_SOURCE.exists()) {
+ unzipRepository(AMBITION_REPO_SOURCE.getPath() + ".zip", AMBITION_REPO_SOURCE.getParentFile());
+ }
+ if (!GITECTIVE_REPO_SOURCE.exists()) {
+ unzipRepository(GITECTIVE_REPO_SOURCE.getPath() + ".zip", GITECTIVE_REPO_SOURCE.getParentFile());
+ }
+ cloneOrFetch("helloworld.git", HELLOWORLD_REPO_SOURCE.getAbsolutePath());
+ cloneOrFetch("ticgit.git", TICGIT_REPO_SOURCE.getAbsolutePath());
+ cloneOrFetch("test/jgit.git", "https://github.com/eclipse-jgit/jgit.git");
+ cloneOrFetch("test/helloworld.git", HELLOWORLD_REPO_SOURCE.getAbsolutePath());
+ cloneOrFetch("test/ambition.git", AMBITION_REPO_SOURCE.getAbsolutePath());
+ cloneOrFetch("test/gitective.git", GITECTIVE_REPO_SOURCE.getAbsolutePath());
+
+ showRemoteBranches("ticgit.git");
+ automaticallyTagBranchTips("ticgit.git");
+ showRemoteBranches("test/jgit.git");
+ automaticallyTagBranchTips("test/jgit.git");
+ }
+ repositoriesInitialized.set(true);
+ }
+ }
+
+ private static void waitForPort(String service, int port) throws Exception {
+ long deadline = System.currentTimeMillis() + 30000L;
+ while (System.currentTimeMillis() < deadline) {
+ try (Socket socket = new Socket()) {
+ socket.connect(new InetSocketAddress("127.0.0.1", port), 500);
+ return;
+ } catch (IOException e) {
+ Thread.sleep(250L);
+ }
+ }
+ throw new IOException("Timed out waiting for " + service + " port " + port);
+ }
private static void showRemoteBranches(String repositoryName) {
try {
diff --git a/src/test/java/com/gitblit/tests/SshDaemonTest.java b/src/test/java/com/gitblit/tests/SshDaemonTest.java
index e88dc9bbf..0af96a831 100644
--- a/src/test/java/com/gitblit/tests/SshDaemonTest.java
+++ b/src/test/java/com/gitblit/tests/SshDaemonTest.java
@@ -17,12 +17,16 @@
import java.io.File;
import java.security.KeyPair;
+import java.util.Arrays;
import java.text.MessageFormat;
import java.util.List;
import org.apache.sshd.client.SshClient;
import org.apache.sshd.client.future.AuthFuture;
import org.apache.sshd.client.session.ClientSession;
+import org.apache.sshd.common.NamedFactory;
+import org.apache.sshd.common.signature.BuiltinSignatures;
+import org.apache.sshd.common.signature.Signature;
import org.eclipse.jgit.api.CloneCommand;
import org.eclipse.jgit.api.Git;
import org.eclipse.jgit.revwalk.RevCommit;
@@ -65,6 +69,16 @@ public void testPublicKeyAuthentication() throws Exception {
assertTrue(authFuture.isSuccess());
}
+ @Test
+ public void testPublicKeyAuthenticationRsaSha256() throws Exception {
+ assertPublicKeyAuthenticationWithSignature(BuiltinSignatures.rsaSHA256, "rsa-sha2-256");
+ }
+
+ @Test
+ public void testPublicKeyAuthenticationRsaSha512() throws Exception {
+ assertPublicKeyAuthenticationWithSignature(BuiltinSignatures.rsaSHA512, "rsa-sha2-512");
+ }
+
@Test
public void testWrongPublicKeyAuthentication() throws Exception {
SshClient client = getClient();
@@ -151,4 +165,22 @@ public void testCloneCommand() throws Exception {
model.authorizationControl = AuthorizationControl.NAMED;
repositories().updateRepositoryModel(model.name, model, false);
}
+
+ private void assertPublicKeyAuthenticationWithSignature(BuiltinSignatures signatureFactory, String signatureName)
+ throws Exception {
+ SignatureCaptureReporter reporter = new SignatureCaptureReporter();
+ List> signatureFactories = Arrays.>asList(signatureFactory);
+ SshClient client = getClient(signatureFactories, reporter);
+ ClientSession session = client.connect(username, "localhost", GitBlitSuite.sshPort).verify().getSession();
+
+ session.addPublicKeyIdentity(rwKeyPair);
+ AuthFuture authFuture = session.auth();
+ assertTrue(authFuture.await());
+ assertTrue(authFuture.isSuccess());
+ assertTrue("client did not attempt " + signatureName,
+ reporter.getAttempts().contains(signatureName) || reporter.getSignatures().contains(signatureName));
+ assertFalse("client fell back to ssh-rsa",
+ reporter.getAttempts().contains("ssh-rsa") || reporter.getSignatures().contains("ssh-rsa"));
+ client.stop();
+ }
}
diff --git a/src/test/java/com/gitblit/tests/SshUnitTest.java b/src/test/java/com/gitblit/tests/SshUnitTest.java
index acb0269c2..bdef7f534 100644
--- a/src/test/java/com/gitblit/tests/SshUnitTest.java
+++ b/src/test/java/com/gitblit/tests/SshUnitTest.java
@@ -25,17 +25,25 @@
import java.security.KeyPair;
import java.security.KeyPairGenerator;
import java.security.PublicKey;
+import java.util.ArrayList;
+import java.util.Collections;
import java.util.EnumSet;
+import java.util.List;
import java.util.concurrent.atomic.AtomicBoolean;
import org.apache.sshd.client.SshClient;
+import org.apache.sshd.client.auth.pubkey.PublicKeyAuthenticationReporter;
import org.apache.sshd.client.channel.ClientChannel;
import org.apache.sshd.client.channel.ClientChannelEvent;
import org.apache.sshd.client.config.keys.ClientIdentityLoader;
import org.apache.sshd.client.future.AuthFuture;
import org.apache.sshd.client.keyverifier.ServerKeyVerifier;
import org.apache.sshd.client.session.ClientSession;
+import org.apache.sshd.common.NamedFactory;
import org.apache.sshd.common.config.keys.FilePasswordProvider;
+import org.apache.sshd.common.NamedResource;
+import org.apache.sshd.common.session.SessionContext;
+import org.apache.sshd.common.signature.Signature;
import org.apache.sshd.common.util.security.SecurityUtils;
import org.eclipse.jgit.lib.Config;
import org.eclipse.jgit.storage.file.FileBasedConfig;
@@ -45,6 +53,7 @@
import org.junit.AfterClass;
import org.junit.Before;
import org.junit.BeforeClass;
+import org.junit.Ignore;
import com.gitblit.Constants.AccessPermission;
import com.gitblit.transport.ssh.IPublicKeyManager;
@@ -54,6 +63,7 @@
/**
* Base class for SSH unit tests.
*/
+@Ignore("abstract ssh test base")
public abstract class SshUnitTest extends GitblitUnitTest {
protected static final AtomicBoolean started = new AtomicBoolean(false);
@@ -156,15 +166,28 @@ public void tearDown() {
}
protected SshClient getClient() {
+ return getClient(null, null);
+ }
+
+ protected SshClient getClient(List> signatureFactories,
+ PublicKeyAuthenticationReporter publicKeyAuthenticationReporter) {
SshClient client = SshClient.setUpDefaultClient();
+ if (signatureFactories != null) {
+ client.setSignatureFactories(signatureFactories);
+ }
+ if (publicKeyAuthenticationReporter != null) {
+ client.setPublicKeyAuthenticationReporter(publicKeyAuthenticationReporter);
+ }
client.setClientIdentityLoader(new ClientIdentityLoader() { // Ignore the files under ~/.ssh
@Override
- public boolean isValidLocation(String location) throws IOException {
- return true;
+ public boolean isValidLocation(NamedResource location) throws IOException {
+ return false;
}
+
@Override
- public KeyPair loadClientIdentity(String location, FilePasswordProvider provider) throws IOException, GeneralSecurityException {
- return null;
+ public Iterable loadClientIdentities(SessionContext session, NamedResource location, FilePasswordProvider provider)
+ throws IOException, GeneralSecurityException {
+ return Collections.emptyList();
}
});
client.setServerKeyVerifier(new ServerKeyVerifier() {
@@ -177,6 +200,48 @@ public boolean verifyServerKey(ClientSession sshClientSession, SocketAddress rem
return client;
}
+ protected static class SignatureCaptureReporter implements PublicKeyAuthenticationReporter {
+ private final List attempts = new ArrayList();
+ private final List signatures = new ArrayList();
+
+ @Override
+ public void signalAuthenticationAttempt(ClientSession session, String service, KeyPair identity, String signature)
+ throws Exception {
+ attempts.add(signature);
+ }
+
+ @Override
+ public void signalAuthenticationExhausted(ClientSession session, String service) throws Exception {
+ }
+
+ @Override
+ public void signalIdentitySkipped(ClientSession session, String service, KeyPair identity) throws Exception {
+ }
+
+ @Override
+ public void signalSignatureAttempt(ClientSession session, String service, KeyPair identity, String signature,
+ byte[] signedData) throws Exception {
+ signatures.add(signature);
+ }
+
+ @Override
+ public void signalAuthenticationSuccess(ClientSession session, String service, KeyPair identity) throws Exception {
+ }
+
+ @Override
+ public void signalAuthenticationFailure(ClientSession session, String service, KeyPair identity, boolean partial,
+ List serverMethods) throws Exception {
+ }
+
+ public List getAttempts() {
+ return attempts;
+ }
+
+ public List getSignatures() {
+ return signatures;
+ }
+ }
+
protected String testSshCommand(String cmd) throws IOException, InterruptedException {
return testSshCommand(cmd, null);
}
@@ -202,9 +267,8 @@ protected String testSshCommand(String cmd, String stdin) throws IOException, In
ByteArrayOutputStream err = new ByteArrayOutputStream();
channel.setOut(out);
channel.setErr(err);
- channel.open();
-
- channel.waitFor(EnumSet.of(ClientChannelEvent.CLOSED, ClientChannelEvent.EOF), 0);
+ channel.open().verify();
+ channel.waitFor(EnumSet.of(ClientChannelEvent.CLOSED), 30000);
String result = out.toString().trim();
channel.close(false);