| // 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.google.gerrit.sshd; |
| |
| import com.google.common.base.Strings; |
| import com.google.common.base.Throwables; |
| import com.google.common.collect.Sets; |
| import com.google.common.util.concurrent.Atomics; |
| import com.google.gerrit.extensions.registration.DynamicSet; |
| import com.google.gerrit.extensions.restapi.AuthException; |
| import com.google.gerrit.server.args4j.SubcommandHandler; |
| import com.google.gerrit.server.permissions.GlobalPermission; |
| import com.google.gerrit.server.permissions.PermissionBackend; |
| import com.google.gerrit.server.permissions.PermissionBackendException; |
| import com.google.inject.Inject; |
| import com.google.inject.assistedinject.Assisted; |
| import java.io.IOException; |
| import java.io.StringWriter; |
| import java.util.ArrayList; |
| import java.util.List; |
| import java.util.Map; |
| import java.util.concurrent.atomic.AtomicReference; |
| import org.apache.sshd.server.Environment; |
| import org.apache.sshd.server.channel.ChannelSession; |
| import org.apache.sshd.server.command.Command; |
| import org.kohsuke.args4j.Argument; |
| |
| /** Command that dispatches to a subcommand from its command table. */ |
| final class DispatchCommand extends BaseCommand { |
| interface Factory { |
| DispatchCommand create(Map<String, CommandProvider> map); |
| } |
| |
| private final PermissionBackend permissionBackend; |
| private final Map<String, CommandProvider> commands; |
| private final AtomicReference<Command> atomicCmd; |
| private final DynamicSet<SshExecuteCommandInterceptor> commandInterceptors; |
| |
| @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<>(); |
| |
| @Inject |
| DispatchCommand( |
| PermissionBackend permissionBackend, |
| DynamicSet<SshExecuteCommandInterceptor> commandInterceptors, |
| @Assisted Map<String, CommandProvider> all) { |
| this.permissionBackend = permissionBackend; |
| commands = all; |
| atomicCmd = Atomics.newReference(); |
| this.commandInterceptors = commandInterceptors; |
| } |
| |
| Map<String, CommandProvider> getMap() { |
| return commands; |
| } |
| |
| @Override |
| public void start(ChannelSession channel, Environment env) throws IOException { |
| try { |
| parseCommandLine(); |
| if (Strings.isNullOrEmpty(commandName)) { |
| StringWriter msg = new StringWriter(); |
| msg.write(usage()); |
| throw die(msg.toString()); |
| } |
| |
| final CommandProvider p = commands.get(commandName); |
| if (p == null) { |
| String msg = |
| (getName().isEmpty() ? "Gerrit Code Review" : getName()) |
| + ": " |
| + commandName |
| + ": not found"; |
| throw die(msg); |
| } |
| |
| final Command cmd = p.getProvider().get(); |
| checkRequiresCapability(cmd); |
| String actualCommandName = commandName; |
| if (cmd instanceof BaseCommand) { |
| final BaseCommand bc = (BaseCommand) cmd; |
| if (!getName().isEmpty()) { |
| actualCommandName = getName() + " " + commandName; |
| } |
| bc.setName(actualCommandName); |
| bc.setArguments(args.toArray(new String[args.size()])); |
| |
| } else if (!args.isEmpty()) { |
| throw die(commandName + " does not take arguments"); |
| } |
| |
| for (SshExecuteCommandInterceptor commandInterceptor : commandInterceptors) { |
| if (!commandInterceptor.accept(actualCommandName, args)) { |
| throw new UnloggedFailure( |
| 126, |
| String.format( |
| "blocked by %s, contact gerrit administrators for more details", |
| commandInterceptor.name())); |
| } |
| } |
| |
| provideStateTo(cmd); |
| atomicCmd.set(cmd); |
| cmd.start(channel, env); |
| |
| } catch (UnloggedFailure e) { |
| String msg = e.getMessage(); |
| if (!msg.endsWith("\n")) { |
| msg += "\n"; |
| } |
| err.write(msg.getBytes(ENC)); |
| err.flush(); |
| onExit(e.exitCode); |
| } |
| } |
| |
| private void checkRequiresCapability(Command cmd) throws UnloggedFailure { |
| String pluginName = null; |
| if (cmd instanceof BaseCommand) { |
| pluginName = ((BaseCommand) cmd).getPluginName(); |
| } |
| try { |
| permissionBackend |
| .currentUser() |
| .checkAny(GlobalPermission.fromAnnotation(pluginName, cmd.getClass())); |
| } catch (AuthException e) { |
| throw new UnloggedFailure(BaseCommand.STATUS_NOT_ADMIN, e.getMessage()); |
| } catch (PermissionBackendException e) { |
| throw new UnloggedFailure(1, "fatal: permission check unavailable", e); |
| } |
| } |
| |
| @Override |
| public void destroy(ChannelSession channel) { |
| Command cmd = atomicCmd.getAndSet(null); |
| if (cmd != null) { |
| try { |
| cmd.destroy(channel); |
| } catch (Exception e) { |
| Throwables.throwIfUnchecked(e); |
| throw new RuntimeException(e); |
| } |
| } |
| } |
| |
| @Override |
| 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; |
| for (String name : commands.keySet()) { |
| maxLength = Math.max(maxLength, name.length()); |
| } |
| String format = "%-" + maxLength + "s %s"; |
| for (String name : Sets.newTreeSet(commands.keySet())) { |
| final CommandProvider p = commands.get(name); |
| usage.append(" "); |
| usage.append(String.format(format, name, Strings.nullToEmpty(p.getDescription()))); |
| 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(); |
| } |
| |
| public String getCommandName() { |
| return commandName; |
| } |
| } |