summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorDavid Ostrovsky <david@ostrovsky.org>2014-02-17 21:56:36 +0100
committerJames Moger <james.moger@gitblit.com>2014-04-10 18:58:07 -0400
commit7613df52959b6e2ac1094d2263be310fb3e2723b (patch)
treef0a644a1256dc8665555d94a6d0bd813661c7809
parent41124cddb6edd82c1630efb99b29c839304ed897 (diff)
downloadgitblit-7613df52959b6e2ac1094d2263be310fb3e2723b.tar.gz
gitblit-7613df52959b6e2ac1094d2263be310fb3e2723b.zip
SSHD: Add support for generic commands
Change-Id: I5a60710323ca674d70e34f7451422ec167105429
-rw-r--r--src/main/java/com/gitblit/transport/ssh/AbstractSshCommand.java11
-rw-r--r--src/main/java/com/gitblit/transport/ssh/CommandDispatcher.java44
-rw-r--r--src/main/java/com/gitblit/transport/ssh/CommandMetaData.java31
-rw-r--r--src/main/java/com/gitblit/transport/ssh/SshCommandFactory.java255
-rw-r--r--src/main/java/com/gitblit/transport/ssh/SshCommandServer.java12
-rw-r--r--src/main/java/com/gitblit/transport/ssh/SshDaemon.java80
-rw-r--r--src/main/java/com/gitblit/transport/ssh/SshKeyAuthenticator.java116
-rw-r--r--src/main/java/com/gitblit/transport/ssh/SshKeyCacheEntry.java26
-rw-r--r--src/main/java/com/gitblit/transport/ssh/SshSession.java102
-rw-r--r--src/main/java/com/gitblit/transport/ssh/commands/BaseCommand.java430
-rw-r--r--src/main/java/com/gitblit/transport/ssh/commands/CreateRepository.java36
-rw-r--r--src/main/java/com/gitblit/transport/ssh/commands/DispatchCommand.java156
-rw-r--r--src/main/java/com/gitblit/transport/ssh/commands/SshCommand.java45
-rw-r--r--src/main/java/com/gitblit/transport/ssh/commands/VersionCommand.java (renamed from src/main/java/com/gitblit/transport/ssh/SshDaemonClient.java)30
-rw-r--r--src/main/java/com/gitblit/utils/IdGenerator.java91
-rw-r--r--src/main/java/com/gitblit/utils/TaskInfoFactory.java19
-rw-r--r--src/main/java/com/gitblit/utils/WorkQueue.java340
-rw-r--r--src/main/java/com/gitblit/utils/cli/CmdLineParser.java440
-rw-r--r--src/main/java/com/gitblit/utils/cli/SubcommandHandler.java43
-rw-r--r--src/main/java/log4j.properties1
20 files changed, 2239 insertions, 69 deletions
diff --git a/src/main/java/com/gitblit/transport/ssh/AbstractSshCommand.java b/src/main/java/com/gitblit/transport/ssh/AbstractSshCommand.java
index e4741ed0..a6681f5c 100644
--- a/src/main/java/com/gitblit/transport/ssh/AbstractSshCommand.java
+++ b/src/main/java/com/gitblit/transport/ssh/AbstractSshCommand.java
@@ -15,9 +15,12 @@
*/
package com.gitblit.transport.ssh;
+import java.io.BufferedWriter;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
+import java.io.OutputStreamWriter;
+import java.io.PrintWriter;
import org.apache.sshd.server.Command;
import org.apache.sshd.server.Environment;
@@ -25,12 +28,14 @@ import org.apache.sshd.server.ExitCallback;
import org.apache.sshd.server.SessionAware;
import org.apache.sshd.server.session.ServerSession;
+import com.google.common.base.Charsets;
+
/**
*
* @author Eric Myrhe
*
*/
-abstract class AbstractSshCommand implements Command, SessionAware {
+public abstract class AbstractSshCommand implements Command, SessionAware {
protected InputStream in;
@@ -70,6 +75,10 @@ abstract class AbstractSshCommand implements Command, SessionAware {
@Override
public void destroy() {}
+ 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;
}
diff --git a/src/main/java/com/gitblit/transport/ssh/CommandDispatcher.java b/src/main/java/com/gitblit/transport/ssh/CommandDispatcher.java
new file mode 100644
index 00000000..18c1c331
--- /dev/null
+++ b/src/main/java/com/gitblit/transport/ssh/CommandDispatcher.java
@@ -0,0 +1,44 @@
+package com.gitblit.transport.ssh;
+
+import java.util.Map;
+import java.util.Set;
+
+import javax.inject.Inject;
+import javax.inject.Named;
+import javax.inject.Provider;
+
+import org.apache.sshd.server.Command;
+
+import com.gitblit.transport.ssh.commands.DispatchCommand;
+import com.google.common.collect.Maps;
+import com.google.common.collect.Sets;
+
+public class CommandDispatcher extends DispatchCommand {
+
+ Provider<Command> repo;
+ Provider<Command> version;
+
+ @Inject
+ public CommandDispatcher(final @Named("create-repository") Provider<Command> repo,
+ final @Named("version") Provider<Command> version) {
+ this.repo = repo;
+ this.version = version;
+ }
+
+ public DispatchCommand get() {
+ DispatchCommand root = new DispatchCommand();
+ Map<String, Provider<Command>> origin = Maps.newHashMapWithExpectedSize(2);
+ origin.put("gitblit", new Provider<Command>() {
+ @Override
+ public Command get() {
+ Set<Provider<Command>> gitblit = Sets.newHashSetWithExpectedSize(2);
+ gitblit.add(repo);
+ gitblit.add(version);
+ Command cmd = new DispatchCommand(gitblit);
+ return cmd;
+ }
+ });
+ root.setMap(origin);
+ return root;
+ }
+}
diff --git a/src/main/java/com/gitblit/transport/ssh/CommandMetaData.java b/src/main/java/com/gitblit/transport/ssh/CommandMetaData.java
new file mode 100644
index 00000000..52231b3b
--- /dev/null
+++ b/src/main/java/com/gitblit/transport/ssh/CommandMetaData.java
@@ -0,0 +1,31 @@
+//Copyright (C) 2013 The Android Open Source Project
+//
+//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 static java.lang.annotation.RetentionPolicy.RUNTIME;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.Target;
+
+/**
+* Annotation tagged on a concrete Command to describe what it is doing
+*/
+@Target({ElementType.TYPE})
+@Retention(RUNTIME)
+public @interface CommandMetaData {
+String name();
+String description() default "";
+}
diff --git a/src/main/java/com/gitblit/transport/ssh/SshCommandFactory.java b/src/main/java/com/gitblit/transport/ssh/SshCommandFactory.java
index c0b4930d..85c503d4 100644
--- a/src/main/java/com/gitblit/transport/ssh/SshCommandFactory.java
+++ b/src/main/java/com/gitblit/transport/ssh/SshCommandFactory.java
@@ -16,11 +16,23 @@
package com.gitblit.transport.ssh;
import java.io.IOException;
-import java.util.Scanner;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.Future;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.concurrent.atomic.AtomicReference;
+
+import javax.inject.Inject;
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.session.ServerSession;
import org.eclipse.jgit.errors.RepositoryNotFoundException;
import org.eclipse.jgit.lib.Repository;
import org.eclipse.jgit.transport.PacketLineOut;
@@ -31,8 +43,13 @@ import org.eclipse.jgit.transport.resolver.ReceivePackFactory;
import org.eclipse.jgit.transport.resolver.ServiceNotAuthorizedException;
import org.eclipse.jgit.transport.resolver.ServiceNotEnabledException;
import org.eclipse.jgit.transport.resolver.UploadPackFactory;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
import com.gitblit.git.RepositoryResolver;
+import com.gitblit.transport.ssh.commands.DispatchCommand;
+import com.gitblit.utils.WorkQueue;
+import com.google.common.util.concurrent.Atomics;
/**
*
@@ -40,31 +57,233 @@ import com.gitblit.git.RepositoryResolver;
*
*/
public class SshCommandFactory implements CommandFactory {
- public SshCommandFactory(RepositoryResolver<SshDaemonClient> repositoryResolver, UploadPackFactory<SshDaemonClient> uploadPackFactory, ReceivePackFactory<SshDaemonClient> receivePackFactory) {
+ private static final Logger logger = LoggerFactory
+ .getLogger(SshCommandFactory.class);
+ private RepositoryResolver<SshSession> repositoryResolver;
+
+ private UploadPackFactory<SshSession> uploadPackFactory;
+
+ private ReceivePackFactory<SshSession> receivePackFactory;
+ private final ScheduledExecutorService startExecutor;
+
+ private CommandDispatcher dispatcher;
+
+ @Inject
+ public SshCommandFactory(RepositoryResolver<SshSession> repositoryResolver,
+ UploadPackFactory<SshSession> uploadPackFactory,
+ ReceivePackFactory<SshSession> receivePackFactory,
+ WorkQueue workQueue,
+ CommandDispatcher d) {
this.repositoryResolver = repositoryResolver;
this.uploadPackFactory = uploadPackFactory;
this.receivePackFactory = receivePackFactory;
+ this.dispatcher = d;
+ int threads = 2;//cfg.getInt("sshd","commandStartThreads", 2);
+ startExecutor = workQueue.createQueue(threads, "SshCommandStart");
}
- private RepositoryResolver<SshDaemonClient> repositoryResolver;
-
- private UploadPackFactory<SshDaemonClient> uploadPackFactory;
-
- private ReceivePackFactory<SshDaemonClient> receivePackFactory;
-
@Override
public Command createCommand(final String commandLine) {
- Scanner commandScanner = new Scanner(commandLine);
- final String command = commandScanner.next();
- final String argument = commandScanner.nextLine();
-
+ return new Trampoline(commandLine);
+ /*
if ("git-upload-pack".equals(command))
return new UploadPackCommand(argument);
if ("git-receive-pack".equals(command))
return new ReceivePackCommand(argument);
return new NonCommand();
+ */
}
+ private class Trampoline implements Command, SessionAware {
+ private final String[] argv;
+ private InputStream in;
+ private OutputStream out;
+ private OutputStream err;
+ private ExitCallback exit;
+ private Environment env;
+ private DispatchCommand cmd;
+ private final AtomicBoolean logged;
+ private final AtomicReference<Future<?>> task;
+
+ Trampoline(final String cmdLine) {
+ argv = split(cmdLine);
+ logged = new AtomicBoolean();
+ task = Atomics.newReference();
+ }
+
+ @Override
+ public void setSession(ServerSession session) {
+ // TODO Auto-generated method stub
+ }
+
+ public void setInputStream(final InputStream in) {
+ this.in = in;
+ }
+
+ public void setOutputStream(final OutputStream out) {
+ this.out = out;
+ }
+
+ public void setErrorStream(final OutputStream err) {
+ this.err = err;
+ }
+
+ public void setExitCallback(final ExitCallback callback) {
+ this.exit = callback;
+ }
+
+ public void start(final Environment env) throws IOException {
+ this.env = env;
+ task.set(startExecutor.submit(new Runnable() {
+ public void run() {
+ try {
+ onStart();
+ } catch (Exception e) {
+ logger.warn("Cannot start command ", e);
+ }
+ }
+
+ @Override
+ public String toString() {
+ //return "start (user " + ctx.getSession().getUsername() + ")";
+ return "start (user TODO)";
+ }
+ }));
+ }
+
+ private void onStart() throws IOException {
+ synchronized (this) {
+ //final Context old = sshScope.set(ctx);
+ try {
+ cmd = dispatcher.get();
+ 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);
+ }
+
+ @Override
+ public void onExit(int rc) {
+ exit.onExit(translateExit(rc));
+ log(rc);
+ }
+ });
+ cmd.start(env);
+ } finally {
+ //sshScope.set(old);
+ }
+ }
+ }
+
+ private int translateExit(final int rc) {
+ return rc;
+//
+// switch (rc) {
+// case BaseCommand.STATUS_NOT_ADMIN:
+// return 1;
+//
+// case BaseCommand.STATUS_CANCEL:
+// return 15 /* SIGKILL */;
+//
+// case BaseCommand.STATUS_NOT_FOUND:
+// return 127 /* POSIX not found */;
+//
+// default:
+// return rc;
+// }
+
+ }
+
+ private void log(final int rc) {
+ if (logged.compareAndSet(false, true)) {
+ //log.onExecute(cmd, rc);
+ 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) {
+ //final Context old = sshScope.set(ctx);
+ try {
+ cmd.destroy();
+ //log(BaseCommand.STATUS_CANCEL);
+ } finally {
+ //ctx = null;
+ cmd = null;
+ //sshScope.set(old);
+ }
+ }
+ }
+ }
+ }
+
+ /** Split a command line into a string array. */
+ static public String[] split(String commandLine) {
+ final List<String> list = new ArrayList<String>();
+ 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 abstract class RepositoryCommand extends AbstractSshCommand {
protected final String repositoryName;
@@ -76,7 +295,7 @@ public class SshCommandFactory implements CommandFactory {
public void start(Environment env) throws IOException {
Repository db = null;
try {
- SshDaemonClient client = session.getAttribute(SshDaemonClient.ATTR_KEY);
+ SshSession client = session.getAttribute(SshSession.KEY);
db = selectRepository(client, repositoryName);
if (db == null) return;
run(client, db);
@@ -92,7 +311,7 @@ public class SshCommandFactory implements CommandFactory {
}
}
- protected Repository selectRepository(SshDaemonClient client, String name) throws IOException {
+ protected Repository selectRepository(SshSession client, String name) throws IOException {
try {
return openRepository(client, name);
} catch (ServiceMayNotContinueException e) {
@@ -104,7 +323,7 @@ public class SshCommandFactory implements CommandFactory {
}
}
- protected Repository openRepository(SshDaemonClient client, String name)
+ protected Repository openRepository(SshSession client, String name)
throws ServiceMayNotContinueException {
// Assume any attempt to use \ was by a Windows client
// and correct to the more typical / used in Git URIs.
@@ -129,7 +348,7 @@ public class SshCommandFactory implements CommandFactory {
}
}
- protected abstract void run(SshDaemonClient client, Repository db)
+ protected abstract void run(SshSession client, Repository db)
throws IOException, ServiceNotEnabledException, ServiceNotAuthorizedException;
}
@@ -137,7 +356,7 @@ public class SshCommandFactory implements CommandFactory {
public UploadPackCommand(String repositoryName) { super(repositoryName); }
@Override
- protected void run(SshDaemonClient client, Repository db)
+ protected void run(SshSession client, Repository db)
throws IOException, ServiceNotEnabledException, ServiceNotAuthorizedException {
UploadPack up = uploadPackFactory.create(client, db);
up.upload(in, out, null);
@@ -148,7 +367,7 @@ public class SshCommandFactory implements CommandFactory {
public ReceivePackCommand(String repositoryName) { super(repositoryName); }
@Override
- protected void run(SshDaemonClient client, Repository db)
+ protected void run(SshSession client, Repository db)
throws IOException, ServiceNotEnabledException, ServiceNotAuthorizedException {
ReceivePack rp = receivePackFactory.create(client, db);
rp.receive(in, out, null);
diff --git a/src/main/java/com/gitblit/transport/ssh/SshCommandServer.java b/src/main/java/com/gitblit/transport/ssh/SshCommandServer.java
index 26e3d67e..7186737f 100644
--- a/src/main/java/com/gitblit/transport/ssh/SshCommandServer.java
+++ b/src/main/java/com/gitblit/transport/ssh/SshCommandServer.java
@@ -17,12 +17,15 @@ package com.gitblit.transport.ssh;
import java.io.IOException;
import java.net.InetSocketAddress;
+import java.net.SocketAddress;
import java.security.InvalidKeyException;
import java.util.Arrays;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
+import javax.inject.Inject;
+
import org.apache.mina.core.future.IoFuture;
import org.apache.mina.core.future.IoFutureListener;
import org.apache.mina.core.session.IoSession;
@@ -69,6 +72,8 @@ import org.apache.sshd.server.session.SessionFactory;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
+import com.gitblit.utils.IdGenerator;
+
/**
*
* @author Eric Myhre
@@ -78,7 +83,8 @@ public class SshCommandServer extends SshServer {
private static final Logger log = LoggerFactory.getLogger(SshCommandServer.class);
- public SshCommandServer() {
+ @Inject
+ public SshCommandServer(final IdGenerator idGenerator) {
setSessionFactory(new SessionFactory() {
@Override
protected ServerSession createSession(final IoSession io) throws Exception {
@@ -90,7 +96,9 @@ public class SshCommandServer extends SshServer {
}
final ServerSession s = (ServerSession) super.createSession(io);
- s.setAttribute(SshDaemonClient.ATTR_KEY, new SshDaemonClient());
+ SocketAddress peer = io.getRemoteAddress();
+ SshSession session = new SshSession(idGenerator.next(), peer);
+ s.setAttribute(SshSession.KEY, session);
io.getCloseFuture().addListener(new IoFutureListener<IoFuture>() {
@Override
diff --git a/src/main/java/com/gitblit/transport/ssh/SshDaemon.java b/src/main/java/com/gitblit/transport/ssh/SshDaemon.java
index 6f5d5f9e..056735a1 100644
--- a/src/main/java/com/gitblit/transport/ssh/SshDaemon.java
+++ b/src/main/java/com/gitblit/transport/ssh/SshDaemon.java
@@ -21,6 +21,10 @@ import java.net.InetSocketAddress;
import java.text.MessageFormat;
import java.util.concurrent.atomic.AtomicBoolean;
+import javax.inject.Named;
+import javax.inject.Singleton;
+
+import org.apache.sshd.server.Command;
import org.apache.sshd.server.keyprovider.PEMGeneratorHostKeyProvider;
import org.eclipse.jgit.internal.JGitText;
import org.eclipse.jgit.transport.resolver.ReceivePackFactory;
@@ -34,8 +38,15 @@ import com.gitblit.git.GitblitReceivePackFactory;
import com.gitblit.git.GitblitUploadPackFactory;
import com.gitblit.git.RepositoryResolver;
import com.gitblit.manager.IGitblit;
+import com.gitblit.transport.ssh.commands.CreateRepository;
+import com.gitblit.transport.ssh.commands.VersionCommand;
+import com.gitblit.utils.IdGenerator;
import com.gitblit.utils.StringUtils;
+import dagger.Module;
+import dagger.ObjectGraph;
+import dagger.Provides;
+
/**
* Manager for the ssh transport. Roughly analogous to the
* {@link com.gitblit.git.GitDaemon} class.
@@ -62,11 +73,7 @@ public class SshDaemon {
private SshCommandServer sshd;
- private RepositoryResolver<SshDaemonClient> repositoryResolver;
-
- private UploadPackFactory<SshDaemonClient> uploadPackFactory;
-
- private ReceivePackFactory<SshDaemonClient> receivePackFactory;
+ private IGitblit gitblit;
/**
* Construct the Gitblit SSH daemon.
@@ -75,6 +82,7 @@ public class SshDaemon {
*/
public SshDaemon(IGitblit gitblit) {
+ this.gitblit = gitblit;
IStoredSettings settings = gitblit.getSettings();
int port = settings.getInteger(Keys.git.sshPort, 0);
String bindInterface = settings.getString(Keys.git.sshBindInterface, "localhost");
@@ -85,7 +93,8 @@ public class SshDaemon {
myAddress = new InetSocketAddress(bindInterface, port);
}
- sshd = new SshCommandServer();
+ ObjectGraph graph = ObjectGraph.create(new SshModule());
+ sshd = graph.get(SshCommandServer.class);
sshd.setPort(myAddress.getPort());
sshd.setHost(myAddress.getHostName());
sshd.setup();
@@ -93,15 +102,8 @@ public class SshDaemon {
sshd.setPublickeyAuthenticator(new SshKeyAuthenticator(gitblit));
run = new AtomicBoolean(false);
- repositoryResolver = new RepositoryResolver<SshDaemonClient>(gitblit);
- uploadPackFactory = new GitblitUploadPackFactory<SshDaemonClient>(gitblit);
- receivePackFactory = new GitblitReceivePackFactory<SshDaemonClient>(gitblit);
-
- sshd.setCommandFactory(new SshCommandFactory(
- repositoryResolver,
- uploadPackFactory,
- receivePackFactory
- ));
+ SshCommandFactory f = graph.get(SshCommandFactory.class);
+ sshd.setCommandFactory(f);
}
public int getPort() {
@@ -156,4 +158,52 @@ public class SshDaemon {
}
}
}
+
+ @Module(library = true,
+ injects = {
+ IGitblit.class,
+ SshCommandFactory.class,
+ SshCommandServer.class,
+ })
+ public class SshModule {
+ @Provides @Named("create-repository") Command provideCreateRepository() {
+ return new CreateRepository();
+ }
+
+ @Provides @Named("version") Command provideVersion() {
+ return new VersionCommand();
+ }
+
+// @Provides(type=Type.SET) @Named("git") Command provideVersionCommand2() {
+// return new CreateRepository();
+// }
+
+// @Provides @Named("git") DispatchCommand providesGitCommand() {
+// return new DispatchCommand("git");
+// }
+
+// @Provides (type=Type.SET) Provider<Command> provideNonCommand() {
+// return new SshCommandFactory.NonCommand();
+// }
+
+ @Provides @Singleton IdGenerator provideIdGenerator() {
+ return new IdGenerator();
+ }
+
+ @Provides @Singleton RepositoryResolver<SshSession> provideRepositoryResolver() {
+ return new RepositoryResolver<SshSession>(provideGitblit());
+ }
+
+ @Provides @Singleton UploadPackFactory<SshSession> provideUploadPackFactory() {
+ return new GitblitUploadPackFactory<SshSession>(provideGitblit());
+ }
+
+ @Provides @Singleton ReceivePackFactory<SshSession> provideReceivePackFactory() {
+ return new GitblitReceivePackFactory<SshSession>(provideGitblit());
+ }
+
+ @Provides @Singleton IGitblit provideGitblit() {
+ return SshDaemon.this.gitblit;
+ }
+ }
}
diff --git a/src/main/java/com/gitblit/transport/ssh/SshKeyAuthenticator.java b/src/main/java/com/gitblit/transport/ssh/SshKeyAuthenticator.java
index 4c97c58d..4ab20f33 100644
--- a/src/main/java/com/gitblit/transport/ssh/SshKeyAuthenticator.java
+++ b/src/main/java/com/gitblit/transport/ssh/SshKeyAuthenticator.java
@@ -1,26 +1,39 @@
/*
* Copyright 2014 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
+ * 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
+ * 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.
+ * 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.io.File;
+import java.io.IOException;
import java.security.PublicKey;
+import java.util.Locale;
+import java.util.concurrent.ExecutionException;
+import org.apache.commons.codec.binary.Base64;
+import org.apache.sshd.common.util.Buffer;
import org.apache.sshd.server.PublickeyAuthenticator;
import org.apache.sshd.server.session.ServerSession;
+import org.eclipse.jgit.lib.Constants;
import com.gitblit.manager.IGitblit;
+import com.google.common.base.Charsets;
+import com.google.common.cache.CacheBuilder;
+import com.google.common.cache.CacheLoader;
+import com.google.common.cache.LoadingCache;
+import com.google.common.cache.Weigher;
+import com.google.common.io.Files;
/**
*
@@ -29,15 +42,84 @@ import com.gitblit.manager.IGitblit;
*/
public class SshKeyAuthenticator implements PublickeyAuthenticator {
- protected final IGitblit gitblit;
+ protected final IGitblit gitblit;
- public SshKeyAuthenticator(IGitblit gitblit) {
- this.gitblit = gitblit;
- }
+ LoadingCache<String, SshKeyCacheEntry> sshKeyCache = CacheBuilder
+ .newBuilder().maximumWeight(2 << 20).weigher(new SshKeyCacheWeigher())
+ .build(new CacheLoader<String, SshKeyCacheEntry>() {
+ public SshKeyCacheEntry load(String key) throws Exception {
+ return loadKey(key);
+ }
- @Override
- public boolean authenticate(String username, PublicKey key, ServerSession session) {
- // TODO actually authenticate
- return true;
- }
+ private SshKeyCacheEntry loadKey(String key) {
+ try {
+ // TODO(davido): retrieve absolute path to public key directory:
+ //String dir = gitblit.getSettings().getString("public_key_dir", "data/ssh");
+ String dir = "/tmp/";
+ // Expect public key file name in form: <username.pub> in
+ File file = new File(dir + key + ".pub");
+ String str = Files.toString(file, Charsets.ISO_8859_1);
+ final String[] parts = str.split(" ");
+ final byte[] bin =
+ Base64.decodeBase64(Constants.encodeASCII(parts[1]));
+ return new SshKeyCacheEntry(key, new Buffer(bin).getRawPublicKey());
+ } catch (IOException e) {
+ throw new RuntimeException("Canot read public key", e);
+ }
+ }
+ });
+
+ public SshKeyAuthenticator(IGitblit gitblit) {
+ this.gitblit = gitblit;
+ }
+
+ @Override
+ public boolean authenticate(String username, final PublicKey suppliedKey,
+ final ServerSession session) {
+ final SshSession sd = session.getAttribute(SshSession.KEY);
+
+ // if (config.getBoolean("auth", "userNameToLowerCase", false)) {
+ username = username.toLowerCase(Locale.US);
+ // }
+ try {
+ // TODO: allow multiple public keys per user
+ SshKeyCacheEntry key = sshKeyCache.get(username);
+ if (key == null) {
+ sd.authenticationError(username, "no-matching-key");
+ return false;
+ }
+
+ if (key.match(suppliedKey)) {
+ return success(username, session, sd);
+ }
+ return false;
+ } catch (ExecutionException e) {
+ sd.authenticationError(username, "user-not-found");
+ return false;
+ }
+ }
+
+ boolean success(String username, ServerSession session, SshSession sd) {
+ sd.authenticationSuccess(username);
+ /*
+ * sshLog.onLogin();
+ *
+ * GerritServerSession s = (GerritServerSession) session;
+ * s.addCloseSessionListener( new SshFutureListener<CloseFuture>() {
+ *
+ * @Override public void operationComplete(CloseFuture future) { final
+ * Context ctx = sshScope.newContext(null, sd, null); final Context old =
+ * sshScope.set(ctx); try { sshLog.onLogout(); } finally {
+ * sshScope.set(old); } } }); }
+ */
+ return true;
+ }
+
+ private static class SshKeyCacheWeigher implements
+ Weigher<String, SshKeyCacheEntry> {
+ @Override
+ public int weigh(String key, SshKeyCacheEntry value) {
+ return key.length() + value.weigh();
+ }
+ }
}
diff --git a/src/main/java/com/gitblit/transport/ssh/SshKeyCacheEntry.java b/src/main/java/com/gitblit/transport/ssh/SshKeyCacheEntry.java
new file mode 100644
index 00000000..ddc48b35
--- /dev/null
+++ b/src/main/java/com/gitblit/transport/ssh/SshKeyCacheEntry.java
@@ -0,0 +1,26 @@
+
+package com.gitblit.transport.ssh;
+
+import java.security.PublicKey;
+
+class SshKeyCacheEntry {
+ private final String user;
+ private final PublicKey publicKey;
+
+ SshKeyCacheEntry(String user, PublicKey publicKey) {
+ this.user = user;
+ this.publicKey = publicKey;
+ }
+
+ String getUser() {
+ return user;
+ }
+
+ boolean match(PublicKey inkey) {
+ return publicKey.equals(inkey);
+ }
+
+ int weigh() {
+ return publicKey.getEncoded().length;
+ }
+}
diff --git a/src/main/java/com/gitblit/transport/ssh/SshSession.java b/src/main/java/com/gitblit/transport/ssh/SshSession.java
new file mode 100644
index 00000000..9f18a197
--- /dev/null
+++ b/src/main/java/com/gitblit/transport/ssh/SshSession.java
@@ -0,0 +1,102 @@
+/*
+ * Copyright 2014 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.net.InetAddress;
+import java.net.InetSocketAddress;
+import java.net.SocketAddress;
+
+import org.apache.sshd.common.Session.AttributeKey;
+
+/**
+ *
+ * @author Eric Myrhe
+ *
+ */
+public class SshSession {
+ public static final AttributeKey<SshSession> KEY =
+ new AttributeKey<SshSession>();
+
+ private final int sessionId;
+ private final SocketAddress remoteAddress;
+ private final String remoteAsString;
+
+ private volatile String username;
+ private volatile String authError;
+
+ SshSession(int sessionId, SocketAddress peer) {
+ this.sessionId = sessionId;
+ this.remoteAddress = peer;
+ this.remoteAsString = format(remoteAddress);
+ }
+
+ public SocketAddress getRemoteAddress() {
+ return remoteAddress;
+ }
+
+ String getRemoteAddressAsString() {
+ return remoteAsString;
+ }
+
+ public String getRemoteUser() {
+ return username;
+ }
+
+ /** Unique session number, assigned during connect. */
+ public int getSessionId() {
+ return sessionId;
+ }
+
+ String getUsername() {
+ return username;
+ }
+
+ String getAuthenticationError() {
+ return authError;
+ }
+
+ void authenticationSuccess(String user) {
+ username = user;
+ authError = null;
+ }
+
+ void authenticationError(String user, String error) {
+ username = user;
+ authError = error;
+ }
+
+ /** @return {@code true} if the authentication did not succeed. */
+ boolean isAuthenticationError() {
+ return authError != null;
+ }
+
+ private static String format(final SocketAddress remote) {
+ if (remote instanceof InetSocketAddress) {
+ final InetSocketAddress sa = (InetSocketAddress) remote;
+
+ final InetAddress in = sa.getAddress();
+ if (in != null) {
+ return in.getHostAddress();
+ }
+
+ final String hostName = sa.getHostName();
+ if (hostName != null) {
+ return hostName;
+ }
+ }
+ return remote.toString();
+ }
+}
diff --git a/src/main/java/com/gitblit/transport/ssh/commands/BaseCommand.java b/src/main/java/com/gitblit/transport/ssh/commands/BaseCommand.java
new file mode 100644
index 00000000..fd73ccfd
--- /dev/null
+++ b/src/main/java/com/gitblit/transport/ssh/commands/BaseCommand.java
@@ -0,0 +1,430 @@
+// Copyright (C) 2009 The Android Open Source Project
+//
+// 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.commands;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InterruptedIOException;
+import java.io.OutputStream;
+import java.io.StringWriter;
+import java.util.concurrent.Future;
+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.kohsuke.args4j.Argument;
+import org.kohsuke.args4j.CmdLineException;
+import org.kohsuke.args4j.Option;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.gitblit.transport.ssh.AbstractSshCommand;
+import com.gitblit.utils.IdGenerator;
+import com.gitblit.utils.WorkQueue;
+import com.gitblit.utils.cli.CmdLineParser;
+import com.google.common.base.Charsets;
+import com.google.common.util.concurrent.Atomics;
+
+public abstract class BaseCommand extends AbstractSshCommand {
+ private static final Logger log = LoggerFactory
+ .getLogger(BaseCommand.class);
+
+ /** 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<Future<?>> task;
+
+ private final WorkQueue.Executor executor;
+
+ public BaseCommand() {
+ task = Atomics.newReference();
+ IdGenerator gen = new IdGenerator();
+ WorkQueue w = new WorkQueue(gen);
+ this.executor = w.getDefaultQueue();
+ }
+
+ public void setInputStream(final InputStream in) {
+ this.in = in;
+ }
+
+ public void setOutputStream(final OutputStream out) {
+ this.out = out;
+ }
+
+ public void setErrorStream(final OutputStream err) {
+ this.err = err;
+ }
+
+ public void setExitCallback(final ExitCallback callback) {
+ this.exit = callback;
+ }
+
+ protected void provideStateTo(final Command cmd) {
+ cmd.setInputStream(in);
+ cmd.setOutputStream(out);
+ cmd.setErrorStream(err);
+ cmd.setExitCallback(exit);
+ }
+
+ 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.
+ * <p>
+ * 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.
+ * <p>
+ * 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()) {
+ StringWriter msg = new StringWriter();
+ clp.printDetailedUsage(commandName, msg);
+ msg.write(usage());
+ 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);
+ }
+
+ protected String usage() {
+ return "";
+ }
+
+ private final class TaskThunk implements com.gitblit.utils.WorkQueue.CancelableRunnable {
+ private final CommandRunnable thunk;
+ private final String taskName;
+
+ private TaskThunk(final CommandRunnable thunk) {
+ this.thunk = thunk;
+
+ // TODO
+// StringBuilder m = new StringBuilder("foo");
+// m.append(context.getCommandLine());
+// if (userProvider.get().isIdentifiedUser()) {
+// IdentifiedUser u = (IdentifiedUser) userProvider.get();
+// m.append(" (").append(u.getAccount().getUserName()).append(")");
+// }
+ this.taskName = "foo";//m.toString();
+ }
+
+ @Override
+ public void cancel() {
+ synchronized (this) {
+ //final Context old = sshScope.set(context);
+ try {
+ //onExit(/*STATUS_CANCEL*/);
+ } finally {
+ //sshScope.set(old);
+ }
+ }
+ }
+
+ @Override
+ public void run() {
+ synchronized (this) {
+ final Thread thisThread = Thread.currentThread();
+ final String thisName = thisThread.getName();
+ int rc = 0;
+ //final Context old = sshScope.set(context);
+ try {
+ //context.started = TimeUtil.nowMs();
+ 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 static interface CommandRunnable {
+ public void run() throws Exception;
+ }
+
+
+ /**
+ * Spawn a function into its own thread.
+ * <p>
+ * Typically this should be invoked within {@link Command#start(Environment)},
+ * such as:
+ *
+ * <pre>
+ * startThread(new Runnable() {
+ * public void run() {
+ * runImp();
+ * }
+ * });
+ * </pre>
+ *
+ * @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.
+ * <p>
+ * Commands should invoke this at most once. Once invoked, the command may
+ * lose access to request based resources as any callbacks previously
+ * registered with {@link RequestCleanup} will fire.
+ *
+ * @param rc exit code for the remote client.
+ */
+ protected void onExit(final int rc) {
+ exit.onExit(rc);
+// if (cleanup != null) {
+// cleanup.run();
+// }
+ }
+
+ 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");
+// if (userProvider.get().isIdentifiedUser()) {
+// final IdentifiedUser u = (IdentifiedUser) userProvider.get();
+// m.append(" (user ");
+// m.append(u.getAccount().getUserName());
+// m.append(" account ");
+// m.append(u.getAccountId());
+// m.append(")");
+// }
+// m.append(" during ");
+// m.append(contextProvider.get().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.
+ * <p>
+ * Typically this should be invoked within {@link Command#start(Environment)},
+ * such as:
+ *
+ * <pre>
+ * startThread(new CommandRunnable() {
+ * public void run() throws Exception {
+ * runImp();
+ * }
+ * });
+ * </pre>
+ * <p>
+ * 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(executor.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);
+ }
+ }
+}
diff --git a/src/main/java/com/gitblit/transport/ssh/commands/CreateRepository.java b/src/main/java/com/gitblit/transport/ssh/commands/CreateRepository.java
new file mode 100644
index 00000000..802905f2
--- /dev/null
+++ b/src/main/java/com/gitblit/transport/ssh/commands/CreateRepository.java
@@ -0,0 +1,36 @@
+/*
+ * Copyright 2014 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.commands;
+
+import org.kohsuke.args4j.Option;
+
+import com.gitblit.transport.ssh.CommandMetaData;
+
+@CommandMetaData(name = "create-repository", description = "Create new GIT repository")
+public class CreateRepository extends SshCommand {
+
+ @Option(name = "--name", aliases = {"-n"}, required = true, metaVar = "NAME", usage = "name of repository to be created")
+ private String name;
+
+ @Option(name = "--description", aliases = {"-d"}, metaVar = "DESCRIPTION", usage = "description of repository")
+ private String repositoryDescription;
+
+ @Override
+ public void run() {
+ stdout.println(String.format("Repository <%s> was created", name));
+ }
+}
diff --git a/src/main/java/com/gitblit/transport/ssh/commands/DispatchCommand.java b/src/main/java/com/gitblit/transport/ssh/commands/DispatchCommand.java
new file mode 100644
index 00000000..672f0245
--- /dev/null
+++ b/src/main/java/com/gitblit/transport/ssh/commands/DispatchCommand.java
@@ -0,0 +1,156 @@
+// Copyright (C) 2009 The Android Open Source Project
+//
+// 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.commands;
+
+import java.io.IOException;
+import java.io.StringWriter;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+import javax.inject.Provider;
+
+import org.apache.sshd.server.Command;
+import org.apache.sshd.server.Environment;
+import org.kohsuke.args4j.Argument;
+
+import com.gitblit.transport.ssh.CommandMetaData;
+import com.gitblit.utils.cli.SubcommandHandler;
+import com.google.common.base.Charsets;
+import com.google.common.base.Strings;
+import com.google.common.collect.Maps;
+import com.google.common.collect.Sets;
+
+public class DispatchCommand extends BaseCommand {
+
+ @Argument(index = 0, required = false, metaVar = "COMMAND", handler = SubcommandHandler.class)
+ private String commandName;
+
+ @Argument(index = 1, multiValued = true, metaVar = "ARG")
+ private List<String> args = new ArrayList<String>();
+
+ private Set<Provider<Command>> commands;
+ private Map<String, Provider<Command>> map;
+
+ public DispatchCommand() {}
+
+ public DispatchCommand(Map<String, Provider<Command>> map) {
+ this.map = map;
+ }
+
+ public void setMap(Map<String, Provider<Command>> m) {
+ map = m;
+ }
+
+ public DispatchCommand(Set<Provider<Command>> commands) {
+ this.commands = commands;
+ }
+
+ private Map<String, Provider<Command>> getMap() {
+ if (map == null) {
+ map = Maps.newHashMapWithExpectedSize(commands.size());
+ for (Provider<Command> cmd : commands) {
+ CommandMetaData meta = cmd.get().getClass().getAnnotation(CommandMetaData.class);
+ map.put(meta.name(), cmd);
+ }
+ }
+ 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());
+ }
+
+ final Provider<Command> p = getMap().get(commandName);
+ if (p == null) {
+ String msg =
+ (getName().isEmpty() ? "Gitblit" : getName()) + ": "
+ + commandName + ": not found";
+ throw new UnloggedFailure(1, msg);
+ }
+
+ final Command cmd = p.get();
+ if (cmd instanceof BaseCommand) {
+ BaseCommand bc = (BaseCommand) cmd;
+ if (getName().isEmpty()) {
+ bc.setName(commandName);
+ } else {
+ bc.setName(getName() + " " + commandName);
+ }
+ bc.setArguments(args.toArray(new String[args.size()]));
+ } else if (!args.isEmpty()) {
+ throw new UnloggedFailure(1, commandName + " does not take arguments");
+ }
+
+ 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);
+ }
+ }
+
+ protected String usage() {
+ final StringBuilder usage = new StringBuilder();
+ usage.append("Available commands");
+ if (!getName().isEmpty()) {
+ usage.append(" of ");
+ usage.append(getName());
+ }
+ usage.append(" are:\n");
+ usage.append("\n");
+
+ int maxLength = -1;
+ Map<String, Provider<Command>> m = getMap();
+ for (String name : m.keySet()) {
+ maxLength = Math.max(maxLength, name.length());
+ }
+ String format = "%-" + maxLength + "s %s";
+ for (String name : Sets.newTreeSet(m.keySet())) {
+ final Provider<Command> p = m.get(name);
+ usage.append(" ");
+ CommandMetaData meta = p.get().getClass().getAnnotation(CommandMetaData.class);
+ if (meta != null) {
+ usage.append(String.format(format, name,
+ Strings.nullToEmpty(meta.description())));
+ }
+ usage.append("\n");
+ }
+ usage.append("\n");
+
+ usage.append("See '");
+ if (getName().indexOf(' ') < 0) {
+ usage.append(getName());
+ usage.append(' ');
+ }
+ usage.append("COMMAND --help' for more information.\n");
+ usage.append("\n");
+ return usage.toString();
+ }
+}
diff --git a/src/main/java/com/gitblit/transport/ssh/commands/SshCommand.java b/src/main/java/com/gitblit/transport/ssh/commands/SshCommand.java
new file mode 100644
index 00000000..44618f3b
--- /dev/null
+++ b/src/main/java/com/gitblit/transport/ssh/commands/SshCommand.java
@@ -0,0 +1,45 @@
+// Copyright (C) 2012 The Android Open Source Project
+//
+// 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.commands;
+
+import java.io.IOException;
+import java.io.PrintWriter;
+
+import org.apache.sshd.server.Environment;
+
+public abstract class SshCommand extends BaseCommand {
+ protected PrintWriter stdout;
+ protected PrintWriter stderr;
+
+ @Override
+ public void start(Environment env) throws IOException {
+ startThread(new CommandRunnable() {
+ @Override
+ public void run() throws Exception {
+ parseCommandLine();
+ stdout = toPrintWriter(out);
+ stderr = toPrintWriter(err);
+ try {
+ SshCommand.this.run();
+ } finally {
+ stdout.flush();
+ stderr.flush();
+ }
+ }
+ });
+ }
+
+ protected abstract void run() throws UnloggedFailure, Failure, Exception;
+}
diff --git a/src/main/java/com/gitblit/transport/ssh/SshDaemonClient.java b/src/main/java/com/gitblit/transport/ssh/commands/VersionCommand.java
index 2e8008ac..baae6a2c 100644
--- a/src/main/java/com/gitblit/transport/ssh/SshDaemonClient.java
+++ b/src/main/java/com/gitblit/transport/ssh/commands/VersionCommand.java
@@ -13,25 +13,23 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-package com.gitblit.transport.ssh;
-import java.net.InetAddress;
+package com.gitblit.transport.ssh.commands;
-import org.apache.sshd.common.Session.AttributeKey;
+import org.kohsuke.args4j.Option;
-/**
- *
- * @author Eric Myrhe
- *
- */
-public class SshDaemonClient {
- public static final AttributeKey<SshDaemonClient> ATTR_KEY = new AttributeKey<SshDaemonClient>();
+import com.gitblit.Constants;
+import com.gitblit.transport.ssh.CommandMetaData;
+
+@CommandMetaData(name="version", description = "Print Gitblit version")
+public class VersionCommand extends SshCommand {
- public InetAddress getRemoteAddress() {
- return null;
- }
+ @Option(name = "--verbose", aliases = {"-v"}, metaVar = "VERBOSE", usage = "Print verbose versions")
+ private boolean verbose;
- public String getRemoteUser() {
- return null;
- }
+ @Override
+ public void run() {
+ stdout.println(String.format("Version: %s", Constants.getGitBlitVersion(),
+ verbose));
+ }
}
diff --git a/src/main/java/com/gitblit/utils/IdGenerator.java b/src/main/java/com/gitblit/utils/IdGenerator.java
new file mode 100644
index 00000000..d2c1cb23
--- /dev/null
+++ b/src/main/java/com/gitblit/utils/IdGenerator.java
@@ -0,0 +1,91 @@
+// Copyright (C) 2009 The Android Open Source Project
+//
+// 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.utils;
+
+import java.util.Random;
+import java.util.concurrent.atomic.AtomicInteger;
+
+import javax.inject.Inject;
+
+/** Simple class to produce 4 billion keys randomly distributed. */
+public class IdGenerator {
+ /** Format an id created by this class as a hex string. */
+ public static String format(int id) {
+ final char[] r = new char[8];
+ for (int p = 7; 0 <= p; p--) {
+ final int h = id & 0xf;
+ r[p] = h < 10 ? (char) ('0' + h) : (char) ('a' + (h - 10));
+ id >>= 4;
+ }
+ return new String(r);
+ }
+
+ private final AtomicInteger gen;
+
+ @Inject
+ public IdGenerator() {
+ gen = new AtomicInteger(new Random().nextInt());
+ }
+
+ /** Produce the next identifier. */
+ public int next() {
+ return mix(gen.getAndIncrement());
+ }
+
+ private static final int salt = 0x9e3779b9;
+
+ static int mix(int in) {
+ return mix(salt, in);
+ }
+
+ /** A very simple bit permutation to mask a simple incrementer. */
+ public static int mix(final int salt, final int in) {
+ short v0 = hi16(in);
+ short v1 = lo16(in);
+ v0 += ((v1 << 2) + 0 ^ v1) + (salt ^ (v1 >>> 3)) + 1;
+ v1 += ((v0 << 2) + 2 ^ v0) + (salt ^ (v0 >>> 3)) + 3;
+ return result(v0, v1);
+ }
+
+ /* For testing only. */
+ static int unmix(final int in) {
+ short v0 = hi16(in);
+ short v1 = lo16(in);
+ v1 -= ((v0 << 2) + 2 ^ v0) + (salt ^ (v0 >>> 3)) + 3;
+ v0 -= ((v1 << 2) + 0 ^ v1) + (salt ^ (v1 >>> 3)) + 1;
+ return result(v0, v1);
+ }
+
+ private static short hi16(final int in) {
+ return (short) ( //
+ ((in >>> 24 & 0xff)) | //
+ ((in >>> 16 & 0xff) << 8) //
+ );
+ }
+
+ private static short lo16(final int in) {
+ return (short) ( //
+ ((in >>> 8 & 0xff)) | //
+ ((in & 0xff) << 8) //
+ );
+ }
+
+ private static int result(final short v0, final short v1) {
+ return ((v0 & 0xff) << 24) | //
+ (((v0 >>> 8) & 0xff) << 16) | //
+ ((v1 & 0xff) << 8) | //
+ ((v1 >>> 8) & 0xff);
+ }
+}
diff --git a/src/main/java/com/gitblit/utils/TaskInfoFactory.java b/src/main/java/com/gitblit/utils/TaskInfoFactory.java
new file mode 100644
index 00000000..111af27b
--- /dev/null
+++ b/src/main/java/com/gitblit/utils/TaskInfoFactory.java
@@ -0,0 +1,19 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// 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.utils;
+
+public interface TaskInfoFactory<T> {
+ T getTaskInfo(WorkQueue.Task<?> task);
+}
diff --git a/src/main/java/com/gitblit/utils/WorkQueue.java b/src/main/java/com/gitblit/utils/WorkQueue.java
new file mode 100644
index 00000000..778e754c
--- /dev/null
+++ b/src/main/java/com/gitblit/utils/WorkQueue.java
@@ -0,0 +1,340 @@
+// Copyright (C) 2009 The Android Open Source Project
+//
+// 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.utils;
+
+import com.google.common.collect.Lists;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.lang.Thread.UncaughtExceptionHandler;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Date;
+import java.util.List;
+import java.util.concurrent.Callable;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.CopyOnWriteArrayList;
+import java.util.concurrent.Delayed;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.Executors;
+import java.util.concurrent.RunnableScheduledFuture;
+import java.util.concurrent.ScheduledThreadPoolExecutor;
+import java.util.concurrent.ThreadFactory;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.concurrent.atomic.AtomicInteger;
+
+import javax.inject.Inject;
+
+/** Delayed execution of tasks using a background thread pool. */
+public class WorkQueue {
+ private static final Logger log = LoggerFactory.getLogger(WorkQueue.class);
+ private static final UncaughtExceptionHandler LOG_UNCAUGHT_EXCEPTION =
+ new UncaughtExceptionHandler() {
+ @Override
+ public void uncaughtException(Thread t, Throwable e) {
+ log.error("WorkQueue thread " + t.getName() + " threw exception", e);
+ }
+ };
+
+ private Executor defaultQueue;
+ private final IdGenerator idGenerator;
+ private final CopyOnWriteArrayList<Executor> queues;
+
+ @Inject
+ public WorkQueue(final IdGenerator idGenerator) {
+ this.idGenerator = idGenerator;
+ this.queues = new CopyOnWriteArrayList<Executor>();
+ }
+
+ /** Get the default work queue, for miscellaneous tasks. */
+ public synchronized Executor getDefaultQueue() {
+ if (defaultQueue == null) {
+ defaultQueue = createQueue(1, "WorkQueue");
+ }
+ return defaultQueue;
+ }
+
+ /** Create a new executor queue with one thread. */
+ public Executor createQueue(final int poolsize, final String prefix) {
+ final Executor r = new Executor(poolsize, prefix);
+ r.setContinueExistingPeriodicTasksAfterShutdownPolicy(false);
+ r.setExecuteExistingDelayedTasksAfterShutdownPolicy(false);
+ queues.add(r);
+ return r;
+ }
+
+ /** Get all of the tasks currently scheduled in any work queue. */
+ public List<Task<?>> getTasks() {
+ final List<Task<?>> r = new ArrayList<Task<?>>();
+ for (final Executor e : queues) {
+ e.addAllTo(r);
+ }
+ return r;
+ }
+
+ public <T> List<T> getTaskInfos(TaskInfoFactory<T> factory) {
+ List<T> taskInfos = Lists.newArrayList();
+ for (Executor exe : queues) {
+ for (Task<?> task : exe.getTasks()) {
+ taskInfos.add(factory.getTaskInfo(task));
+ }
+ }
+ return taskInfos;
+ }
+
+ /** Locate a task by its unique id, null if no task matches. */
+ public Task<?> getTask(final int id) {
+ Task<?> result = null;
+ for (final Executor e : queues) {
+ final Task<?> t = e.getTask(id);
+ if (t != null) {
+ if (result != null) {
+ // Don't return the task if we have a duplicate. Lie instead.
+ return null;
+ } else {
+ result = t;
+ }
+ }
+ }
+ return result;
+ }
+
+ public void stop() {
+ for (final Executor p : queues) {
+ p.shutdown();
+ boolean isTerminated;
+ do {
+ try {
+ isTerminated = p.awaitTermination(10, TimeUnit.SECONDS);
+ } catch (InterruptedException ie) {
+ isTerminated = false;
+ }
+ } while (!isTerminated);
+ }
+ queues.clear();
+ }
+
+ /** An isolated queue. */
+ public class Executor extends ScheduledThreadPoolExecutor {
+ private final ConcurrentHashMap<Integer, Task<?>> all;
+
+ Executor(final int corePoolSize, final String prefix) {
+ super(corePoolSize, new ThreadFactory() {
+ private final ThreadFactory parent = Executors.defaultThreadFactory();
+ private final AtomicInteger tid = new AtomicInteger(1);
+
+ @Override
+ public Thread newThread(final Runnable task) {
+ final Thread t = parent.newThread(task);
+ t.setName(prefix + "-" + tid.getAndIncrement());
+ t.setUncaughtExceptionHandler(LOG_UNCAUGHT_EXCEPTION);
+ return t;
+ }
+ });
+
+ all = new ConcurrentHashMap<Integer, Task<?>>( //
+ corePoolSize << 1, // table size
+ 0.75f, // load factor
+ corePoolSize + 4 // concurrency level
+ );
+ }
+
+ public void unregisterWorkQueue() {
+ queues.remove(this);
+ }
+
+ @Override
+ protected <V> RunnableScheduledFuture<V> decorateTask(
+ final Runnable runnable, RunnableScheduledFuture<V> r) {
+ r = super.decorateTask(runnable, r);
+ for (;;) {
+ final int id = idGenerator.next();
+
+ Task<V> task;
+ task = new Task<V>(runnable, r, this, id);
+
+ if (all.putIfAbsent(task.getTaskId(), task) == null) {
+ return task;
+ }
+ }
+ }
+
+ @Override
+ protected <V> RunnableScheduledFuture<V> decorateTask(
+ final Callable<V> callable, final RunnableScheduledFuture<V> task) {
+ throw new UnsupportedOperationException("Callable not implemented");
+ }
+
+ void remove(final Task<?> task) {
+ all.remove(task.getTaskId(), task);
+ }
+
+ Task<?> getTask(final int id) {
+ return all.get(id);
+ }
+
+ void addAllTo(final List<Task<?>> list) {
+ list.addAll(all.values()); // iterator is thread safe
+ }
+
+ Collection<Task<?>> getTasks() {
+ return all.values();
+ }
+ }
+
+ /** Runnable needing to know it was canceled. */
+ public interface CancelableRunnable extends Runnable {
+ /** Notifies the runnable it was canceled. */
+ public void cancel();
+ }
+
+ /** A wrapper around a scheduled Runnable, as maintained in the queue. */
+ public static class Task<V> implements RunnableScheduledFuture<V> {
+ /**
+ * Summarized status of a single task.
+ * <p>
+ * Tasks have the following state flow:
+ * <ol>
+ * <li>{@link #SLEEPING}: if scheduled with a non-zero delay.</li>
+ * <li>{@link #READY}: waiting for an available worker thread.</li>
+ * <li>{@link #RUNNING}: actively executing on a worker thread.</li>
+ * <li>{@link #DONE}: finished executing, if not periodic.</li>
+ * </ol>
+ */
+ public static enum State {
+ // Ordered like this so ordinal matches the order we would
+ // prefer to see tasks sorted in: done before running,
+ // running before ready, ready before sleeping.
+ //
+ DONE, CANCELLED, RUNNING, READY, SLEEPING, OTHER
+ }
+
+ private final Runnable runnable;
+ private final RunnableScheduledFuture<V> task;
+ private final Executor executor;
+ private final int taskId;
+ private final AtomicBoolean running;
+ private final Date startTime;
+
+ Task(Runnable runnable, RunnableScheduledFuture<V> task, Executor executor,
+ int taskId) {
+ this.runnable = runnable;
+ this.task = task;
+ this.executor = executor;
+ this.taskId = taskId;
+ this.running = new AtomicBoolean();
+ this.startTime = new Date();
+ }
+
+ public int getTaskId() {
+ return taskId;
+ }
+
+ public State getState() {
+ if (isCancelled()) {
+ return State.CANCELLED;
+ } else if (isDone() && !isPeriodic()) {
+ return State.DONE;
+ } else if (running.get()) {
+ return State.RUNNING;
+ }
+
+ final long delay = getDelay(TimeUnit.MILLISECONDS);
+ if (delay <= 0) {
+ return State.READY;
+ } else if (0 < delay) {
+ return State.SLEEPING;
+ }
+
+ return State.OTHER;
+ }
+
+ public Date getStartTime() {
+ return startTime;
+ }
+
+ public boolean cancel(boolean mayInterruptIfRunning) {
+ if (task.cancel(mayInterruptIfRunning)) {
+ // Tiny abuse of running: if the task needs to know it was
+ // canceled (to clean up resources) and it hasn't started
+ // yet the task's run method won't execute. So we tag it
+ // as running and allow it to clean up. This ensures we do
+ // not invoke cancel twice.
+ //
+ if (runnable instanceof CancelableRunnable
+ && running.compareAndSet(false, true)) {
+ ((CancelableRunnable) runnable).cancel();
+ }
+ executor.remove(this);
+ executor.purge();
+ return true;
+
+ } else {
+ return false;
+ }
+ }
+
+ public int compareTo(Delayed o) {
+ return task.compareTo(o);
+ }
+
+ public V get() throws InterruptedException, ExecutionException {
+ return task.get();
+ }
+
+ public V get(long timeout, TimeUnit unit) throws InterruptedException,
+ ExecutionException, TimeoutException {
+ return task.get(timeout, unit);
+ }
+
+ public long getDelay(TimeUnit unit) {
+ return task.getDelay(unit);
+ }
+
+ public boolean isCancelled() {
+ return task.isCancelled();
+ }
+
+ public boolean isDone() {
+ return task.isDone();
+ }
+
+ public boolean isPeriodic() {
+ return task.isPeriodic();
+ }
+
+ public void run() {
+ if (running.compareAndSet(false, true)) {
+ try {
+ task.run();
+ } finally {
+ if (isPeriodic()) {
+ running.set(false);
+ } else {
+ executor.remove(this);
+ }
+ }
+ }
+ }
+
+ @Override
+ public String toString() {
+ return runnable.toString();
+ }
+ }
+}
diff --git a/src/main/java/com/gitblit/utils/cli/CmdLineParser.java b/src/main/java/com/gitblit/utils/cli/CmdLineParser.java
new file mode 100644
index 00000000..def76df4
--- /dev/null
+++ b/src/main/java/com/gitblit/utils/cli/CmdLineParser.java
@@ -0,0 +1,440 @@
+/*
+ * Copyright (C) 2008, Shawn O. Pearce <spearce@spearce.org>
+ *
+ * (Taken from JGit org.eclipse.jgit.pgm.opt.CmdLineParser.)
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * - Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ *
+ * - Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * - Neither the name of the Git Development Community nor the names of its
+ * contributors may be used to endorse or promote products derived from this
+ * software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
+ * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+ * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+ * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+ * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+ * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+ * POSSIBILITY OF SUCH DAMAGE.
+ */
+
+package com.gitblit.utils.cli;
+
+import java.io.StringWriter;
+import java.io.Writer;
+import java.lang.annotation.Annotation;
+import java.lang.reflect.AnnotatedElement;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+import java.util.ResourceBundle;
+
+import org.kohsuke.args4j.Argument;
+import org.kohsuke.args4j.CmdLineException;
+import org.kohsuke.args4j.IllegalAnnotationError;
+import org.kohsuke.args4j.NamedOptionDef;
+import org.kohsuke.args4j.Option;
+import org.kohsuke.args4j.OptionDef;
+import org.kohsuke.args4j.spi.BooleanOptionHandler;
+import org.kohsuke.args4j.spi.EnumOptionHandler;
+import org.kohsuke.args4j.spi.FieldSetter;
+import org.kohsuke.args4j.spi.OptionHandler;
+import org.kohsuke.args4j.spi.Setter;
+
+import com.google.common.base.Strings;
+import com.google.common.collect.LinkedHashMultimap;
+import com.google.common.collect.Lists;
+import com.google.common.collect.Maps;
+import com.google.common.collect.Multimap;
+
+/**
+ * Extended command line parser which handles --foo=value arguments.
+ * <p>
+ * The args4j package does not natively handle --foo=value and instead prefers
+ * to see --foo value on the command line. Many users are used to the GNU style
+ * --foo=value long option, so we convert from the GNU style format to the
+ * args4j style format prior to invoking args4j for parsing.
+ */
+public class CmdLineParser {
+ public interface Factory {
+ CmdLineParser create(Object bean);
+ }
+
+ private final MyParser parser;
+
+ @SuppressWarnings("rawtypes")
+ private Map<String, OptionHandler> options;
+
+ /**
+ * Creates a new command line owner that parses arguments/options and set them
+ * into the given object.
+ *
+ * @param bean instance of a class annotated by
+ * {@link org.kohsuke.args4j.Option} and
+ * {@link org.kohsuke.args4j.Argument}. this object will receive
+ * values.
+ *
+ * @throws IllegalAnnotationError if the option bean class is using args4j
+ * annotations incorrectly.
+ */
+ public CmdLineParser(Object bean)
+ throws IllegalAnnotationError {
+ this.parser = new MyParser(bean);
+ }
+
+ public void addArgument(Setter<?> setter, Argument a) {
+ parser.addArgument(setter, a);
+ }
+
+ public void addOption(Setter<?> setter, Option o) {
+ parser.addOption(setter, o);
+ }
+
+ public void printSingleLineUsage(Writer w, ResourceBundle rb) {
+ parser.printSingleLineUsage(w, rb);
+ }
+
+ public void printUsage(Writer out, ResourceBundle rb) {
+ parser.printUsage(out, rb);
+ }
+
+ public void printDetailedUsage(String name, StringWriter out) {
+ out.write(name);
+ printSingleLineUsage(out, null);
+ out.write('\n');
+ out.write('\n');
+ printUsage(out, null);
+ out.write('\n');
+ }
+
+ public void printQueryStringUsage(String name, StringWriter out) {
+ out.write(name);
+
+ char next = '?';
+ List<NamedOptionDef> booleans = new ArrayList<NamedOptionDef>();
+ for (@SuppressWarnings("rawtypes") OptionHandler handler : parser.options) {
+ if (handler.option instanceof NamedOptionDef) {
+ NamedOptionDef n = (NamedOptionDef) handler.option;
+
+ if (handler instanceof BooleanOptionHandler) {
+ booleans.add(n);
+ continue;
+ }
+
+ if (!n.required()) {
+ out.write('[');
+ }
+ out.write(next);
+ next = '&';
+ if (n.name().startsWith("--")) {
+ out.write(n.name().substring(2));
+ } else if (n.name().startsWith("-")) {
+ out.write(n.name().substring(1));
+ } else {
+ out.write(n.name());
+ }
+ out.write('=');
+
+ out.write(metaVar(handler, n));
+ if (!n.required()) {
+ out.write(']');
+ }
+ if (n.isMultiValued()) {
+ out.write('*');
+ }
+ }
+ }
+ for (NamedOptionDef n : booleans) {
+ if (!n.required()) {
+ out.write('[');
+ }
+ out.write(next);
+ next = '&';
+ if (n.name().startsWith("--")) {
+ out.write(n.name().substring(2));
+ } else if (n.name().startsWith("-")) {
+ out.write(n.name().substring(1));
+ } else {
+ out.write(n.name());
+ }
+ if (!n.required()) {
+ out.write(']');
+ }
+ }
+ }
+
+ private static String metaVar(OptionHandler<?> handler, NamedOptionDef n) {
+ String var = n.metaVar();
+ if (Strings.isNullOrEmpty(var)) {
+ var = handler.getDefaultMetaVariable();
+ if (handler instanceof EnumOptionHandler) {
+ var = var.substring(1, var.length() - 1).replace(" ", "");
+ }
+ }
+ return var;
+ }
+
+ public boolean wasHelpRequestedByOption() {
+ return parser.help.value;
+ }
+
+ public void parseArgument(final String... args) throws CmdLineException {
+ List<String> tmp = Lists.newArrayListWithCapacity(args.length);
+ for (int argi = 0; argi < args.length; argi++) {
+ final String str = args[argi];
+ if (str.equals("--")) {
+ while (argi < args.length)
+ tmp.add(args[argi++]);
+ break;
+ }
+
+ if (str.startsWith("--")) {
+ final int eq = str.indexOf('=');
+ if (eq > 0) {
+ tmp.add(str.substring(0, eq));
+ tmp.add(str.substring(eq + 1));
+ continue;
+ }
+ }
+
+ tmp.add(str);
+ }
+ parser.parseArgument(tmp.toArray(new String[tmp.size()]));
+ }
+
+ public void parseOptionMap(Map<String, String[]> parameters)
+ throws CmdLineException {
+ Multimap<String, String> map = LinkedHashMultimap.create();
+ for (Map.Entry<String, String[]> ent : parameters.entrySet()) {
+ for (String val : ent.getValue()) {
+ map.put(ent.getKey(), val);
+ }
+ }
+ parseOptionMap(map);
+ }
+
+ public void parseOptionMap(Multimap<String, String> params)
+ throws CmdLineException {
+ List<String> tmp = Lists.newArrayListWithCapacity(2 * params.size());
+ for (final String key : params.keySet()) {
+ String name = makeOption(key);
+
+ if (isBoolean(name)) {
+ boolean on = false;
+ for (String value : params.get(key)) {
+ on = toBoolean(key, value);
+ }
+ if (on) {
+ tmp.add(name);
+ }
+ } else {
+ for (String value : params.get(key)) {
+ tmp.add(name);
+ tmp.add(value);
+ }
+ }
+ }
+ parser.parseArgument(tmp.toArray(new String[tmp.size()]));
+ }
+
+ public boolean isBoolean(String name) {
+ return findHandler(makeOption(name)) instanceof BooleanOptionHandler;
+ }
+
+ private String makeOption(String name) {
+ if (!name.startsWith("-")) {
+ if (name.length() == 1) {
+ name = "-" + name;
+ } else {
+ name = "--" + name;
+ }
+ }
+ return name;
+ }
+
+ @SuppressWarnings("rawtypes")
+ private OptionHandler findHandler(String name) {
+ if (options == null) {
+ options = index(parser.options);
+ }
+ return options.get(name);
+ }
+
+ @SuppressWarnings("rawtypes")
+ private static Map<String, OptionHandler> index(List<OptionHandler> in) {
+ Map<String, OptionHandler> m = Maps.newHashMap();
+ for (OptionHandler handler : in) {
+ if (handler.option instanceof NamedOptionDef) {
+ NamedOptionDef def = (NamedOptionDef) handler.option;
+ if (!def.isArgument()) {
+ m.put(def.name(), handler);
+ for (String alias : def.aliases()) {
+ m.put(alias, handler);
+ }
+ }
+ }
+ }
+ return m;
+ }
+
+ private boolean toBoolean(String name, String value) throws CmdLineException {
+ if ("true".equals(value) || "t".equals(value)
+ || "yes".equals(value) || "y".equals(value)
+ || "on".equals(value)
+ || "1".equals(value)
+ || value == null || "".equals(value)) {
+ return true;
+ }
+
+ if ("false".equals(value) || "f".equals(value)
+ || "no".equals(value) || "n".equals(value)
+ || "off".equals(value)
+ || "0".equals(value)) {
+ return false;
+ }
+
+ throw new CmdLineException(parser, String.format(
+ "invalid boolean \"%s=%s\"", name, value));
+ }
+
+ private class MyParser extends org.kohsuke.args4j.CmdLineParser {
+ @SuppressWarnings("rawtypes")
+ private List<OptionHandler> options;
+ private HelpOption help;
+
+ MyParser(final Object bean) {
+ super(bean);
+ ensureOptionsInitialized();
+ }
+
+ @SuppressWarnings({"unchecked", "rawtypes"})
+ @Override
+ protected OptionHandler createOptionHandler(final OptionDef option,
+ final Setter setter) {
+ if (isHandlerSpecified(option) || isEnum(setter) || isPrimitive(setter)) {
+ return add(super.createOptionHandler(option, setter));
+ }
+
+// OptionHandlerFactory<?> factory = handlers.get(setter.getType());
+// if (factory != null) {
+// return factory.create(this, option, setter);
+// }
+ return add(super.createOptionHandler(option, setter));
+ }
+
+ @SuppressWarnings("rawtypes")
+ private OptionHandler add(OptionHandler handler) {
+ ensureOptionsInitialized();
+ options.add(handler);
+ return handler;
+ }
+
+ private void ensureOptionsInitialized() {
+ if (options == null) {
+ help = new HelpOption();
+ options = Lists.newArrayList();
+ addOption(help, help);
+ }
+ }
+
+ private boolean isHandlerSpecified(final OptionDef option) {
+ return option.handler() != OptionHandler.class;
+ }
+
+ private <T> boolean isEnum(Setter<T> setter) {
+ return Enum.class.isAssignableFrom(setter.getType());
+ }
+
+ private <T> boolean isPrimitive(Setter<T> setter) {
+ return setter.getType().isPrimitive();
+ }
+ }
+
+ private static class HelpOption implements Option, Setter<Boolean> {
+ private boolean value;
+
+ @Override
+ public String name() {
+ return "--help";
+ }
+
+ @Override
+ public String[] aliases() {
+ return new String[] {"-h"};
+ }
+
+ @Override
+ public String[] depends() {
+ return new String[] {};
+ }
+
+ @Override
+ public boolean hidden() {
+ return false;
+ }
+
+ @Override
+ public String usage() {
+ return "display this help text";
+ }
+
+ @Override
+ public void addValue(Boolean val) {
+ value = val;
+ }
+
+ @Override
+ public Class<? extends OptionHandler<Boolean>> handler() {
+ return BooleanOptionHandler.class;
+ }
+
+ @Override
+ public String metaVar() {
+ return "";
+ }
+
+ @Override
+ public boolean required() {
+ return false;
+ }
+
+ @Override
+ public Class<? extends Annotation> annotationType() {
+ return Option.class;
+ }
+
+ @Override
+ public FieldSetter asFieldSetter() {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public AnnotatedElement asAnnotatedElement() {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public Class<Boolean> getType() {
+ return Boolean.class;
+ }
+
+ @Override
+ public boolean isMultiValued() {
+ return false;
+ }
+ }
+}
diff --git a/src/main/java/com/gitblit/utils/cli/SubcommandHandler.java b/src/main/java/com/gitblit/utils/cli/SubcommandHandler.java
new file mode 100644
index 00000000..b1ace324
--- /dev/null
+++ b/src/main/java/com/gitblit/utils/cli/SubcommandHandler.java
@@ -0,0 +1,43 @@
+// Copyright (C) 2010 The Android Open Source Project
+//
+// 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.utils.cli;
+
+import org.kohsuke.args4j.CmdLineException;
+import org.kohsuke.args4j.CmdLineParser;
+import org.kohsuke.args4j.OptionDef;
+import org.kohsuke.args4j.spi.OptionHandler;
+import org.kohsuke.args4j.spi.Parameters;
+import org.kohsuke.args4j.spi.Setter;
+
+public class SubcommandHandler extends OptionHandler<String> {
+
+ public SubcommandHandler(CmdLineParser parser,
+ OptionDef option, Setter<String> setter) {
+ super(parser, option, setter);
+ }
+
+ @Override
+ public final int parseArguments(final Parameters params)
+ throws CmdLineException {
+ setter.addValue(params.getParameter(0));
+ owner.stopOptionParsing();
+ return 1;
+ }
+
+ @Override
+ public final String getDefaultMetaVariable() {
+ return "COMMAND";
+ }
+}
diff --git a/src/main/java/log4j.properties b/src/main/java/log4j.properties
index c6b5d8c3..115dcd01 100644
--- a/src/main/java/log4j.properties
+++ b/src/main/java/log4j.properties
@@ -25,6 +25,7 @@ log4j.rootCategory=INFO, S
#log4j.logger.net=INFO
#log4j.logger.com.gitblit=DEBUG
+log4j.logger.org.apache.sshd=ERROR
log4j.logger.org.apache.wicket=INFO
log4j.logger.org.apache.wicket.RequestListenerInterface=WARN