Allow suexec to run any command as any user

The suexec command can only be run by a peer daemon, and permits
the daemon to execute another command as a specific user identity.

This is the foundation of allowing writes to be proxied from a
slave server into the master, the slave just needs to SSH into the
master and use suexec in front of the user supplied command line
to perform an action on their behalf on the master.

Unfortunately this means we have to trust the slave process, as it
can become anyone, including an administrator.  A better approach
would be to use agent authentication and authenticate back through
the slave to the user's agent process, but not every user connection
may be using an agent.  In particular batch jobs might be using an
unencrypted key and no agent to authenticate.

Change-Id: Icb8ddb16959f01189a6c0bdfc8fec45cdd99659b
Signed-off-by: Shawn O. Pearce <sop@google.com>
This commit is contained in:
Shawn O. Pearce 2010-01-16 14:27:28 -08:00
parent 1fc80a6eba
commit 2f8b9bc3b7
15 changed files with 411 additions and 148 deletions

View File

@ -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.
* <p>
* 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.
* <p>
* 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<String> list = new ArrayList<String>();
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() + ")");

View File

@ -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<CommandFactory> {
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<CommandFactory> {
Trampoline(final String cmdLine) {
commandLine = cmdLine;
argv = split(cmdLine);
}
public void setInputStream(final InputStream in) {
@ -84,7 +88,8 @@ class CommandFactoryProvider implements Provider<CommandFactory> {
}
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<CommandFactory> {
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<CommandFactory> {
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<CommandFactory> {
}
}
}
/** Split a command line into a string array. */
static String[] split(String commandLine) {
final List<String> list = new ArrayList<String>();
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()]);
}
}

View File

@ -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<IoFuture>() {
@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();

View File

@ -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<IoFuture>() {
@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();

View File

@ -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<String, Provider<Command>> 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<String> args = new ArrayList<String>();
@Inject
DispatchCommand(final Provider<CurrentUser> cu, @Assisted final String pfx,
@Assisted final Map<String, Provider<Command>> 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<Command> 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<Command> 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<String, Provider<Command>> 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();
}
}

View File

@ -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);

View File

@ -54,11 +54,14 @@ class SshLog implements LifecycleListener {
private static final String P_STATUS = "status";
private final Provider<SshSession> session;
private final Provider<Context> context;
private final AsyncAppender async;
@Inject
SshLog(final Provider<SshSession> session, final SitePaths site) {
SshLog(final Provider<SshSession> session, final Provider<Context> 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 + "'";

View File

@ -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 <T> void registerOptionHandler(Class<T> type,

View File

@ -26,17 +26,26 @@ import java.util.Map;
class SshScope {
static class Context {
private final SshSession session;
private final String commandLine;
private final Map<Key<?>, 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<Key<?>, Object>();
created = System.currentTimeMillis();
started = created;
final long now = System.currentTimeMillis();
created = now;
started = now;
finished = now;
}
String getCommandLine() {
return commandLine;
}
synchronized <T> T get(Key<T> key, Provider<T> creator) {

View File

@ -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;

View File

@ -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.
* <p>
* 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<CurrentUser> caller;
private Provider<SshSession> 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<String> args = new ArrayList<String>();
private Command cmd;
@Inject
SuExec(@CommandName(Commands.ROOT) final DispatchCommandProvider dispatcher,
final Provider<CurrentUser> caller, final Provider<SshSession> 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<SocketAddress>() {
@Override
public SocketAddress get() {
return peer;
}
}, accountId));
}
private static String join(List<String> 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;
}
}
}
}

View File

@ -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<SocketAddress> {
@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";
}
}

View File

@ -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<String> {
@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";
}
}

View File

@ -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);
}
}

View File

@ -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) == '-') {