From 436bd3f0ecdee282c503a9eb0f7a240b7a68ff49 Mon Sep 17 00:00:00 2001
From: James Moger <james.moger@gitblit.com>
Date: Fri, 11 Apr 2014 14:51:50 -0400
Subject: [PATCH] Merged #6 "Support serving repositories over the SSH transport"
---
src/main/java/com/gitblit/transport/ssh/commands/SshCommandFactory.java | 277 +++++++++++++++++++++++++++++++++++++++++++++++++++++++
1 files changed, 277 insertions(+), 0 deletions(-)
diff --git a/src/main/java/com/gitblit/transport/ssh/commands/SshCommandFactory.java b/src/main/java/com/gitblit/transport/ssh/commands/SshCommandFactory.java
new file mode 100644
index 0000000..55090fc
--- /dev/null
+++ b/src/main/java/com/gitblit/transport/ssh/commands/SshCommandFactory.java
@@ -0,0 +1,277 @@
+/*
+ * 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 java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.Future;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.concurrent.atomic.AtomicReference;
+
+import org.apache.sshd.server.Command;
+import org.apache.sshd.server.CommandFactory;
+import org.apache.sshd.server.Environment;
+import org.apache.sshd.server.ExitCallback;
+import org.apache.sshd.server.SessionAware;
+import org.apache.sshd.server.session.ServerSession;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.gitblit.Keys;
+import com.gitblit.manager.IGitblit;
+import com.gitblit.transport.ssh.SshDaemonClient;
+import com.gitblit.utils.IdGenerator;
+import com.gitblit.utils.WorkQueue;
+import com.google.common.util.concurrent.Atomics;
+import com.google.common.util.concurrent.ThreadFactoryBuilder;
+
+/**
+ *
+ * @author Eric Myhre
+ *
+ */
+public class SshCommandFactory implements CommandFactory {
+ private static final Logger logger = LoggerFactory.getLogger(SshCommandFactory.class);
+
+ private final IGitblit gitblit;
+ private final ScheduledExecutorService startExecutor;
+ private final ExecutorService destroyExecutor;
+
+ public SshCommandFactory(IGitblit gitblit, IdGenerator idGenerator) {
+ this.gitblit = gitblit;
+
+ int threads = gitblit.getSettings().getInteger(Keys.git.sshCommandStartThreads, 2);
+ WorkQueue workQueue = new WorkQueue(idGenerator);
+ startExecutor = workQueue.createQueue(threads, "SshCommandStart");
+ destroyExecutor = Executors.newSingleThreadExecutor(
+ new ThreadFactoryBuilder()
+ .setNameFormat("SshCommandDestroy-%s")
+ .setDaemon(true)
+ .build());
+ }
+
+ public void stop() {
+ destroyExecutor.shutdownNow();
+ }
+
+ public RootDispatcher createRootDispatcher(SshDaemonClient client, String commandLine) {
+ return new RootDispatcher(gitblit, client, commandLine);
+ }
+
+ @Override
+ public Command createCommand(final String commandLine) {
+ return new Trampoline(commandLine);
+ }
+
+ private class Trampoline implements Command, SessionAware {
+ private final String[] argv;
+ private ServerSession session;
+ private InputStream in;
+ private OutputStream out;
+ private OutputStream err;
+ private ExitCallback exit;
+ private Environment env;
+ private String cmdLine;
+ private DispatchCommand cmd;
+ private final AtomicBoolean logged;
+ private final AtomicReference<Future<?>> task;
+
+ Trampoline(String line) {
+ if (line.startsWith("git-")) {
+ line = "git " + line;
+ }
+ cmdLine = line;
+ argv = split(line);
+ logged = new AtomicBoolean();
+ task = Atomics.newReference();
+ }
+
+ @Override
+ public void setSession(ServerSession session) {
+ this.session = session;
+ }
+
+ @Override
+ public void setInputStream(final InputStream in) {
+ this.in = in;
+ }
+
+ @Override
+ public void setOutputStream(final OutputStream out) {
+ this.out = out;
+ }
+
+ @Override
+ public void setErrorStream(final OutputStream err) {
+ this.err = err;
+ }
+
+ @Override
+ public void setExitCallback(final ExitCallback callback) {
+ this.exit = callback;
+ }
+
+ @Override
+ public void start(final Environment env) throws IOException {
+ this.env = env;
+ task.set(startExecutor.submit(new Runnable() {
+ @Override
+ public void run() {
+ try {
+ onStart();
+ } catch (Exception e) {
+ logger.warn("Cannot start command ", e);
+ }
+ }
+
+ @Override
+ public String toString() {
+ return "start (user " + session.getUsername() + ")";
+ }
+ }));
+ }
+
+ private void onStart() throws IOException {
+ synchronized (this) {
+ SshDaemonClient client = session.getAttribute(SshDaemonClient.KEY);
+ try {
+ cmd = createRootDispatcher(client, cmdLine);
+ cmd.setArguments(argv);
+ cmd.setInputStream(in);
+ cmd.setOutputStream(out);
+ cmd.setErrorStream(err);
+ cmd.setExitCallback(new ExitCallback() {
+ @Override
+ public void onExit(int rc, String exitMessage) {
+ exit.onExit(translateExit(rc), exitMessage);
+ log(rc);
+ }
+
+ @Override
+ public void onExit(int rc) {
+ exit.onExit(translateExit(rc));
+ log(rc);
+ }
+ });
+ cmd.start(env);
+ } finally {
+ client = null;
+ }
+ }
+ }
+
+ private int translateExit(final int rc) {
+ switch (rc) {
+ case BaseCommand.STATUS_NOT_ADMIN:
+ return 1;
+
+ case BaseCommand.STATUS_CANCEL:
+ return 15 /* 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)) {
+ logger.info("onExecute: {} exits with: {}", cmd.getClass().getSimpleName(), rc);
+ }
+ }
+
+ @Override
+ public void destroy() {
+ Future<?> future = task.getAndSet(null);
+ if (future != null) {
+ future.cancel(true);
+ destroyExecutor.execute(new Runnable() {
+ @Override
+ public void run() {
+ onDestroy();
+ }
+ });
+ }
+ }
+
+ private void onDestroy() {
+ synchronized (this) {
+ if (cmd != null) {
+ try {
+ cmd.destroy();
+ } finally {
+ cmd = null;
+ }
+ }
+ }
+ }
+ }
+
+ /** Split a command line into a string array. */
+ static public String[] split(String commandLine) {
+ final List<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()]);
+ }
+}
--
Gitblit v1.9.1