diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/BaseCommand.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/BaseCommand.java
index 563d9cdffe..8abbc33baf 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/BaseCommand.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/BaseCommand.java
@@ -45,8 +45,6 @@ import java.io.OutputStreamWriter;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.io.UnsupportedEncodingException;
-import java.util.ArrayList;
-import java.util.List;
import java.util.concurrent.Future;
public abstract class BaseCommand implements Command {
@@ -61,6 +59,7 @@ public abstract class BaseCommand implements Command {
@Option(name = "--help", usage = "display this help text", aliases = {"-h"})
private boolean help;
+ @SuppressWarnings("unused")
@Option(name = "--", usage = "end of options", handler = EndOfOptionsHandler.class)
private boolean endOfOptions;
@@ -90,10 +89,10 @@ public abstract class BaseCommand implements Command {
private Future> task;
/** Text of the command line which lead up to invoking this instance. */
- protected String commandPrefix = "";
+ private String commandName = "";
- /** Unparsed rest of the command line. */
- protected String commandLine = "";
+ /** Unparsed command line options. */
+ private String[] argv;
public void setInputStream(final InputStream in) {
this.in = in;
@@ -111,21 +110,12 @@ public abstract class BaseCommand implements Command {
this.exit = callback;
}
- public void setCommandPrefix(final String prefix) {
- this.commandPrefix = prefix;
+ void setName(final String prefix) {
+ this.commandName = prefix;
}
- /**
- * Set the command line to be evaluated by this command.
- *
- * If this command is being invoked from a higher level
- * {@link DispatchCommand} then only the portion after the command name (that
- * is, the arguments) is supplied.
- *
- * @param line the command line received from the client.
- */
- public void setCommandLine(final String line) {
- this.commandLine = line;
+ public void setArguments(final String[] argv) {
+ this.argv = argv;
}
@Override
@@ -154,50 +144,16 @@ public abstract class BaseCommand implements Command {
/**
* Parses the command line argument, injecting parsed values into fields.
*
- * This method must be explicitly invoked to cause a parse. When parsing,
- * arguments are split out of and read from the {@link #commandLine} field.
+ * This method must be explicitly invoked to cause a parse.
*
- * @throws Failure if the command line arguments were invalid.
+ * @throws UnloggedFailure if the command line arguments were invalid.
* @see Option
* @see Argument
*/
- protected void parseCommandLine() throws Failure {
- final List list = new ArrayList();
- boolean inquote = 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)
- r.append(b);
- else if (r.length() > 0) {
- list.add(r.toString());
- r = new StringBuilder();
- }
- continue;
- case '\'':
- 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());
- }
-
+ protected void parseCommandLine() throws UnloggedFailure {
final CmdLineParser clp = newCmdLineParser();
try {
- clp.parseArgument(list.toArray(new String[list.size()]));
+ clp.parseArgument(argv);
} catch (IllegalArgumentException err) {
if (!help) {
throw new UnloggedFailure(1, "fatal: " + err.getMessage());
@@ -210,17 +166,22 @@ public abstract class BaseCommand implements Command {
if (help) {
final StringWriter msg = new StringWriter();
- msg.write(commandPrefix);
+ msg.write(commandName);
clp.printSingleLineUsage(msg, null);
msg.write('\n');
msg.write('\n');
clp.printUsage(msg, null);
msg.write('\n');
+ msg.write(usage());
throw new UnloggedFailure(1, msg.toString());
}
}
+ protected String usage() {
+ return "";
+ }
+
/** Construct a new parser for this command's received command line. */
protected CmdLineParser newCmdLineParser() {
return cmdLineParserFactory.create(this);
@@ -347,7 +308,7 @@ public abstract class BaseCommand implements Command {
m.append(")");
}
m.append(" during ");
- m.append(getFullCommandLine());
+ m.append(contextProvider.get().getCommandLine());
log.error(m.toString(), e);
}
@@ -374,20 +335,6 @@ public abstract class BaseCommand implements Command {
}
}
- @Override
- public String toString() {
- return getFullCommandLine();
- }
-
- private String getFullCommandLine() {
- if (commandPrefix.isEmpty())
- return commandLine;
- else if (commandLine.isEmpty())
- return commandPrefix;
- else
- return commandPrefix + " " + commandLine;
- }
-
private final class TaskThunk implements CancelableRunnable {
private final CommandRunnable thunk;
private final Context context;
@@ -398,7 +345,7 @@ public abstract class BaseCommand implements Command {
this.context = contextProvider.get();
StringBuilder m = new StringBuilder();
- m.append(getFullCommandLine());
+ m.append(context.getCommandLine());
if (userProvider.get() instanceof IdentifiedUser) {
IdentifiedUser u = (IdentifiedUser) userProvider.get();
m.append(" (" + u.getAccount().getUserName() + ")");
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/CommandFactoryProvider.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/CommandFactoryProvider.java
index fbe2373d03..ade3aac563 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/CommandFactoryProvider.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/CommandFactoryProvider.java
@@ -28,6 +28,8 @@ import org.apache.sshd.server.session.ServerSession;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
+import java.util.ArrayList;
+import java.util.List;
/**
* Creates a CommandFactory using commands registered by {@link CommandModule}.
@@ -55,6 +57,7 @@ class CommandFactoryProvider implements Provider {
private class Trampoline implements Command, SessionAware {
private final String commandLine;
+ private final String[] argv;
private InputStream in;
private OutputStream out;
private OutputStream err;
@@ -65,6 +68,7 @@ class CommandFactoryProvider implements Provider {
Trampoline(final String cmdLine) {
commandLine = cmdLine;
+ argv = split(cmdLine);
}
public void setInputStream(final InputStream in) {
@@ -84,7 +88,8 @@ class CommandFactoryProvider implements Provider {
}
public void setSession(final ServerSession session) {
- this.ctx = new Context(session.getAttribute(SshSession.KEY));
+ final SshSession s = session.getAttribute(SshSession.KEY);
+ this.ctx = new Context(s, commandLine);
}
public void start(final Environment env) throws IOException {
@@ -92,7 +97,7 @@ class CommandFactoryProvider implements Provider {
final Context old = SshScope.set(ctx);
try {
cmd = dispatcher.get();
- cmd.setCommandLine(commandLine);
+ cmd.setArguments(argv);
cmd.setInputStream(in);
cmd.setOutputStream(out);
cmd.setErrorStream(err);
@@ -135,8 +140,7 @@ class CommandFactoryProvider implements Provider {
private void log(final int rc) {
synchronized (this) {
if (!logged) {
- ctx.finished = System.currentTimeMillis();
- log.onExecute(ctx, commandLine, rc);
+ log.onExecute(rc);
logged = true;
}
}
@@ -159,4 +163,41 @@ class CommandFactoryProvider implements Provider {
}
}
}
+
+ /** Split a command line into a string array. */
+ static String[] split(String commandLine) {
+ final List list = new ArrayList();
+ boolean inquote = 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)
+ r.append(b);
+ else if (r.length() > 0) {
+ list.add(r.toString());
+ r = new StringBuilder();
+ }
+ continue;
+ case '\'':
+ 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()]);
+ }
}
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/DatabasePasswordAuth.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/DatabasePasswordAuth.java
index 5d5572a724..aff64216a9 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/DatabasePasswordAuth.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/DatabasePasswordAuth.java
@@ -77,7 +77,7 @@ class DatabasePasswordAuth implements PasswordAuthenticator {
// session, record a login event in the log and add
// a close listener to record a logout event.
//
- Context ctx = new Context(sd);
+ Context ctx = new Context(sd, null);
Context old = SshScope.set(ctx);
try {
log.onLogin();
@@ -89,7 +89,7 @@ class DatabasePasswordAuth implements PasswordAuthenticator {
new IoFutureListener() {
@Override
public void operationComplete(IoFuture future) {
- final Context ctx = new Context(sd);
+ final Context ctx = new Context(sd, null);
final Context old = SshScope.set(ctx);
try {
log.onLogout();
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/DatabasePubKeyAuth.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/DatabasePubKeyAuth.java
index 8fd9d72fbc..fa320b74b9 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/DatabasePubKeyAuth.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/DatabasePubKeyAuth.java
@@ -156,7 +156,7 @@ class DatabasePubKeyAuth implements PublickeyAuthenticator {
// session, record a login event in the log and add
// a close listener to record a logout event.
//
- Context ctx = new Context(sd);
+ Context ctx = new Context(sd, null);
Context old = SshScope.set(ctx);
try {
sshLog.onLogin();
@@ -168,7 +168,7 @@ class DatabasePubKeyAuth implements PublickeyAuthenticator {
new IoFutureListener() {
@Override
public void operationComplete(IoFuture future) {
- final Context ctx = new Context(sd);
+ final Context ctx = new Context(sd, null);
final Context old = SshScope.set(ctx);
try {
sshLog.onLogout();
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/DispatchCommand.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/DispatchCommand.java
index a5e0b776ac..a421f701d0 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/DispatchCommand.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/DispatchCommand.java
@@ -15,15 +15,18 @@
package com.google.gerrit.sshd;
import com.google.gerrit.server.CurrentUser;
+import com.google.gerrit.sshd.args4j.SubcommandHandler;
import com.google.inject.Inject;
import com.google.inject.Provider;
import com.google.inject.assistedinject.Assisted;
import org.apache.sshd.server.Command;
import org.apache.sshd.server.Environment;
+import org.kohsuke.args4j.Argument;
import java.io.IOException;
-import java.io.UnsupportedEncodingException;
+import java.util.ArrayList;
+import java.util.List;
import java.util.Map;
/**
@@ -39,6 +42,12 @@ final class DispatchCommand extends BaseCommand {
private final Map> commands;
private Command cmd;
+ @Argument(index = 0, required = true, metaVar = "COMMAND", handler = SubcommandHandler.class)
+ private String commandName;
+
+ @Argument(index = 1, multiValued = true, metaVar = "ARG")
+ private List args = new ArrayList();
+
@Inject
DispatchCommand(final Provider cu, @Assisted final String pfx,
@Assisted final Map> all) {
@@ -49,65 +58,58 @@ final class DispatchCommand extends BaseCommand {
@Override
public void start(final Environment env) throws IOException {
- if (commandLine.isEmpty()) {
- usage();
- return;
- }
+ try {
+ parseCommandLine();
- final String name, args;
- int sp = commandLine.indexOf(' ');
- if (0 < sp) {
- name = commandLine.substring(0, sp);
- while (Character.isWhitespace(commandLine.charAt(sp))) {
- sp++;
+ final Provider p = commands.get(commandName);
+ if (p == null) {
+ String msg =
+ (prefix.isEmpty() ? "Gerrit Code Review" : prefix) + ": "
+ + commandName + ": not found";
+ throw new UnloggedFailure(1, msg);
}
- args = commandLine.substring(sp);
- } else {
- name = commandLine;
- args = "";
- }
- if (name.equals("help") || name.equals("--help") || name.equals("-h")) {
- usage();
- return;
- }
-
- final Provider p = commands.get(name);
- if (p != null) {
final Command cmd = p.get();
+ if (isAdminCommand(cmd) && !currentUser.get().isAdministrator()) {
+ final String msg = "fatal: Not a Gerrit administrator";
+ throw new UnloggedFailure(BaseCommand.STATUS_NOT_ADMIN, msg);
+ }
+
+ if (cmd instanceof BaseCommand) {
+ final BaseCommand bc = (BaseCommand) cmd;
+ if (prefix.isEmpty())
+ bc.setName(commandName);
+ else
+ bc.setName(prefix + " " + commandName);
+ bc.setArguments(args.toArray(new String[args.size()]));
+
+ } else if (!args.isEmpty()) {
+ throw new UnloggedFailure(1, commandName + " does not take arguments");
+ }
+
+ provideStateTo(cmd);
+
synchronized (this) {
this.cmd = cmd;
}
-
- if (cmd.getClass().getAnnotation(AdminCommand.class) != null) {
- final CurrentUser u = currentUser.get();
- if (!u.isAdministrator()) {
- err.write("fatal: Not a Gerrit administrator\n".getBytes(ENC));
- err.flush();
- onExit(BaseCommand.STATUS_NOT_ADMIN);
- return;
- }
- }
-
- provideStateTo(cmd);
- if (cmd instanceof BaseCommand) {
- final BaseCommand bc = (BaseCommand) cmd;
- if (commandPrefix.isEmpty())
- bc.setCommandPrefix(name);
- else
- bc.setCommandPrefix(commandPrefix + " " + name);
- bc.setCommandLine(args);
- }
cmd.start(env);
- } else {
- final String msg = prefix + ": " + name + ": not found\n";
+
+ } catch (UnloggedFailure e) {
+ String msg = e.getMessage();
+ if (!msg.endsWith("\n")) {
+ msg += "\n";
+ }
err.write(msg.getBytes(ENC));
err.flush();
- onExit(BaseCommand.STATUS_NOT_FOUND);
+ onExit(e.exitCode);
}
}
+ private boolean isAdminCommand(final Command cmd) {
+ return cmd.getClass().getAnnotation(AdminCommand.class) != null;
+ }
+
@Override
public void destroy() {
synchronized (this) {
@@ -118,13 +120,15 @@ final class DispatchCommand extends BaseCommand {
}
}
- private void usage() throws IOException, UnsupportedEncodingException {
+ @Override
+ protected String usage() {
final StringBuilder usage = new StringBuilder();
- if (prefix.indexOf(' ') < 0) {
- usage.append("usage: " + prefix + " COMMAND [ARGS]\n");
+ usage.append("Available commands");
+ if (!prefix.isEmpty()) {
+ usage.append(" of ");
+ usage.append(prefix);
}
- usage.append("\n");
- usage.append("Available commands of " + prefix + " are:\n");
+ usage.append(" are:\n");
usage.append("\n");
for (Map.Entry> e : commands.entrySet()) {
usage.append(" ");
@@ -140,8 +144,6 @@ final class DispatchCommand extends BaseCommand {
}
usage.append("COMMAND --help' for more information.\n");
usage.append("\n");
- err.write(usage.toString().getBytes("UTF-8"));
- err.flush();
- onExit(1);
+ return usage.toString();
}
}
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/SshDaemon.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/SshDaemon.java
index d21e6e9ed6..9dcfb44f21 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/SshDaemon.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/SshDaemon.java
@@ -108,7 +108,7 @@ import java.util.List;
@Singleton
public class SshDaemon extends SshServer implements SshInfo, LifecycleListener {
private static final int IANA_SSH_PORT = 22;
- private static final int DEFAULT_PORT = 29418;
+ public static final int DEFAULT_PORT = 29418;
private static final Logger log = LoggerFactory.getLogger(SshDaemon.class);
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/SshLog.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/SshLog.java
index d4d79708dd..32d5a076c6 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/SshLog.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/SshLog.java
@@ -54,11 +54,14 @@ class SshLog implements LifecycleListener {
private static final String P_STATUS = "status";
private final Provider session;
+ private final Provider context;
private final AsyncAppender async;
@Inject
- SshLog(final Provider session, final SitePaths site) {
+ SshLog(final Provider session, final Provider context,
+ final SitePaths site) {
this.session = session;
+ this.context = context;
final DailyRollingFileAppender dst = new DailyRollingFileAppender();
dst.setName(LOG_NAME);
@@ -118,7 +121,11 @@ class SshLog implements LifecycleListener {
async.append(event);
}
- void onExecute(final Context ctx, final String commandLine, int exitValue) {
+ void onExecute(int exitValue) {
+ final Context ctx = context.get();
+ ctx.finished = System.currentTimeMillis();
+
+ final String commandLine = ctx.getCommandLine();
String cmd = QuotedString.BOURNE.quote(commandLine);
if (cmd == commandLine) {
cmd = "'" + commandLine + "'";
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/SshModule.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/SshModule.java
index f85a117a16..8e19ad968b 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/SshModule.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/SshModule.java
@@ -33,6 +33,7 @@ import com.google.gerrit.sshd.args4j.AccountGroupIdHandler;
import com.google.gerrit.sshd.args4j.AccountIdHandler;
import com.google.gerrit.sshd.args4j.PatchSetIdHandler;
import com.google.gerrit.sshd.args4j.ProjectControlHandler;
+import com.google.gerrit.sshd.args4j.SocketAddressHandler;
import com.google.gerrit.sshd.commands.DefaultCommandModule;
import com.google.gerrit.sshd.commands.QueryShell;
import com.google.gerrit.util.cli.CmdLineParser;
@@ -53,8 +54,6 @@ import java.net.SocketAddress;
/** Configures standard dependencies for {@link SshDaemon}. */
public class SshModule extends FactoryModule {
- private static final String NAME = "Gerrit Code Review";
-
@Override
protected void configure() {
bindScope(RequestScoped.class, SshScope.REQUEST);
@@ -70,7 +69,7 @@ public class SshModule extends FactoryModule {
factory(PeerDaemonUser.Factory.class);
bind(DispatchCommandProvider.class).annotatedWith(Commands.CMD_ROOT)
- .toInstance(new DispatchCommandProvider(NAME, Commands.CMD_ROOT));
+ .toInstance(new DispatchCommandProvider("", Commands.CMD_ROOT));
bind(CommandFactoryProvider.class);
bind(CommandFactory.class).toProvider(CommandFactoryProvider.class);
@@ -115,6 +114,7 @@ public class SshModule extends FactoryModule {
registerOptionHandler(AccountGroup.Id.class, AccountGroupIdHandler.class);
registerOptionHandler(PatchSet.Id.class, PatchSetIdHandler.class);
registerOptionHandler(ProjectControl.class, ProjectControlHandler.class);
+ registerOptionHandler(SocketAddress.class, SocketAddressHandler.class);
}
private void registerOptionHandler(Class type,
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/SshScope.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/SshScope.java
index b980c18dbb..247948cd40 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/SshScope.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/SshScope.java
@@ -26,17 +26,26 @@ import java.util.Map;
class SshScope {
static class Context {
private final SshSession session;
+ private final String commandLine;
private final Map, Object> map;
final long created;
volatile long started;
volatile long finished;
- Context(final SshSession s) {
+ Context(final SshSession s, final String c) {
session = s;
+ commandLine = c;
map = new HashMap, Object>();
- created = System.currentTimeMillis();
- started = created;
+
+ final long now = System.currentTimeMillis();
+ created = now;
+ started = now;
+ finished = now;
+ }
+
+ String getCommandLine() {
+ return commandLine;
}
synchronized T get(Key key, Provider creator) {
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/SshSession.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/SshSession.java
index b79db521aa..3c4d4f8f19 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/SshSession.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/SshSession.java
@@ -42,6 +42,17 @@ public class SshSession {
this.remoteAsString = format(remoteAddress);
}
+ SshSession(SshSession parent, SocketAddress peer, CurrentUser user) {
+ this.sessionId = parent.sessionId;
+ this.remoteAddress = peer;
+ if (parent.remoteAddress == peer) {
+ this.remoteAsString = parent.remoteAsString;
+ } else {
+ this.remoteAsString = format(peer) + "/" + parent.remoteAsString;
+ }
+ this.identity = user;
+ }
+
/** Unique session number, assigned during connect. */
public int getSessionId() {
return sessionId;
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/SuExec.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/SuExec.java
new file mode 100644
index 0000000000..6300a7e2aa
--- /dev/null
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/SuExec.java
@@ -0,0 +1,145 @@
+// 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.google.gerrit.sshd;
+
+import com.google.gerrit.reviewdb.Account;
+import com.google.gerrit.server.AccessPath;
+import com.google.gerrit.server.CurrentUser;
+import com.google.gerrit.server.IdentifiedUser;
+import com.google.gerrit.server.PeerDaemonUser;
+import com.google.gerrit.sshd.SshScope.Context;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+
+import org.apache.sshd.server.Command;
+import org.apache.sshd.server.Environment;
+import org.kohsuke.args4j.Argument;
+import org.kohsuke.args4j.Option;
+
+import java.io.IOException;
+import java.net.SocketAddress;
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Executes any other command as a different user identity.
+ *
+ * The calling user must be authenticated as a {@link PeerDaemonUser}, which
+ * usually requires public key authentication using this daemon's private host
+ * key, or a key on this daemon's peer host key ring.
+ */
+public final class SuExec extends BaseCommand {
+ private final DispatchCommandProvider dispatcher;
+
+ private Provider caller;
+ private Provider session;
+ private IdentifiedUser.GenericFactory userFactory;
+
+ @Option(name = "--as", required = true)
+ private Account.Id accountId;
+
+ @Option(name = "--from")
+ private SocketAddress peerAddress;
+
+ @Argument(index = 0, multiValued = true, metaVar = "COMMAND")
+ private List args = new ArrayList();
+
+ private Command cmd;
+
+ @Inject
+ SuExec(@CommandName(Commands.ROOT) final DispatchCommandProvider dispatcher,
+ final Provider caller, final Provider session,
+ final IdentifiedUser.GenericFactory userFactory) {
+ this.dispatcher = dispatcher;
+ this.caller = caller;
+ this.session = session;
+ this.userFactory = userFactory;
+ }
+
+ @Override
+ public void start(Environment env) throws IOException {
+ try {
+ if (caller.get() instanceof PeerDaemonUser) {
+ final PeerDaemonUser peer = (PeerDaemonUser) caller.get();
+
+ parseCommandLine();
+
+ final Context ctx = new Context(newSession(), join(args));
+ final Context old = SshScope.set(ctx);
+ try {
+ final BaseCommand cmd = dispatcher.get();
+ cmd.setArguments(args.toArray(new String[args.size()]));
+ provideStateTo(cmd);
+
+ synchronized (this) {
+ this.cmd = cmd;
+ }
+ cmd.start(env);
+ } finally {
+ SshScope.set(old);
+ }
+
+ } else {
+ throw new UnloggedFailure(1, "fatal: Not a peer daemon");
+ }
+ } catch (UnloggedFailure e) {
+ String msg = e.getMessage();
+ if (!msg.endsWith("\n")) {
+ msg += "\n";
+ }
+ err.write(msg.getBytes("UTF-8"));
+ err.flush();
+ onExit(1);
+ }
+ }
+
+ private SshSession newSession() {
+ final SocketAddress peer;
+ if (peerAddress == null) {
+ peer = session.get().getRemoteAddress();
+ } else {
+ peer = peerAddress;
+ }
+
+ return new SshSession(session.get(), peer, userFactory.create(
+ AccessPath.SSH, new Provider() {
+ @Override
+ public SocketAddress get() {
+ return peer;
+ }
+ }, accountId));
+ }
+
+ private static String join(List args) {
+ StringBuilder r = new StringBuilder();
+ for (String a : args) {
+ if (r.length() > 0) {
+ r.append(" ");
+ }
+ r.append(a);
+ }
+ return r.toString();
+ }
+
+ @Override
+ public void destroy() {
+ synchronized (this) {
+ if (cmd != null) {
+ cmd.destroy();
+ cmd = null;
+ }
+ }
+ }
+}
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/args4j/SocketAddressHandler.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/args4j/SocketAddressHandler.java
new file mode 100644
index 0000000000..75defd43a4
--- /dev/null
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/args4j/SocketAddressHandler.java
@@ -0,0 +1,54 @@
+// 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.google.gerrit.sshd.args4j;
+
+import com.google.gerrit.server.util.SocketUtil;
+import com.google.inject.Inject;
+import com.google.inject.assistedinject.Assisted;
+
+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;
+
+import java.net.SocketAddress;
+
+public class SocketAddressHandler extends OptionHandler {
+ @SuppressWarnings("unchecked")
+ @Inject
+ public SocketAddressHandler(@Assisted final CmdLineParser parser,
+ @Assisted final OptionDef option, @Assisted final Setter setter) {
+ super(parser, option, setter);
+ }
+
+ @Override
+ public final int parseArguments(final Parameters params)
+ throws CmdLineException {
+ final String token = params.getParameter(0);
+ try {
+ setter.addValue(SocketUtil.parse(token, 0));
+ } catch (IllegalArgumentException e) {
+ throw new CmdLineException(owner, e.getMessage());
+ }
+ return 1;
+ }
+
+ @Override
+ public final String getDefaultMetaVariable() {
+ return "HOST:PORT";
+ }
+}
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/args4j/SubcommandHandler.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/args4j/SubcommandHandler.java
new file mode 100644
index 0000000000..4eedca0bcd
--- /dev/null
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/args4j/SubcommandHandler.java
@@ -0,0 +1,47 @@
+// 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.google.gerrit.sshd.args4j;
+
+import com.google.inject.Inject;
+import com.google.inject.assistedinject.Assisted;
+
+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 {
+ @SuppressWarnings("unchecked")
+ @Inject
+ public SubcommandHandler(@Assisted final CmdLineParser parser,
+ @Assisted final OptionDef option, @Assisted final Setter 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/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/DefaultCommandModule.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/DefaultCommandModule.java
index d9ebf57899..19e15fe605 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/DefaultCommandModule.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/DefaultCommandModule.java
@@ -18,6 +18,7 @@ import com.google.gerrit.sshd.CommandModule;
import com.google.gerrit.sshd.CommandName;
import com.google.gerrit.sshd.Commands;
import com.google.gerrit.sshd.DispatchCommandProvider;
+import com.google.gerrit.sshd.SuExec;
/** Register the basic commands any Gerrit server should support. */
@@ -52,5 +53,7 @@ public class DefaultCommandModule extends CommandModule {
command("git-upload-pack").to(Commands.key(git, "upload-pack"));
command("git-receive-pack").to(Commands.key(git, "receive-pack"));
command("gerrit-receive-pack").to(Commands.key(git, "receive-pack"));
+
+ command("suexec").to(SuExec.class);
}
}
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/ScpCommand.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/ScpCommand.java
index 46802f5b90..9bdba7005d 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/ScpCommand.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/ScpCommand.java
@@ -56,10 +56,7 @@ final class ScpCommand extends BaseCommand {
private IOException error;
@Override
- public void setCommandLine(final String line) {
- super.setCommandLine(line);
-
- final String[] args = line.split(" ");
+ public void setArguments(final String[] args) {
root = "";
for (int i = 0; i < args.length; i++) {
if (args[i].charAt(0) == '-') {