diff options
author | David Ostrovsky <david@ostrovsky.org> | 2014-02-17 21:56:36 +0100 |
---|---|---|
committer | James Moger <james.moger@gitblit.com> | 2014-04-10 18:58:07 -0400 |
commit | 7613df52959b6e2ac1094d2263be310fb3e2723b (patch) | |
tree | f0a644a1256dc8665555d94a6d0bd813661c7809 | |
parent | 41124cddb6edd82c1630efb99b29c839304ed897 (diff) | |
download | gitblit-7613df52959b6e2ac1094d2263be310fb3e2723b.tar.gz gitblit-7613df52959b6e2ac1094d2263be310fb3e2723b.zip |
SSHD: Add support for generic commands
Change-Id: I5a60710323ca674d70e34f7451422ec167105429
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 |