Line data Source code
1 : // Copyright (C) 2009 The Android Open Source Project 2 : // 3 : // Licensed under the Apache License, Version 2.0 (the "License"); 4 : // you may not use this file except in compliance with the License. 5 : // You may obtain a copy of the License at 6 : // 7 : // http://www.apache.org/licenses/LICENSE-2.0 8 : // 9 : // Unless required by applicable law or agreed to in writing, software 10 : // distributed under the License is distributed on an "AS IS" BASIS, 11 : // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 : // See the License for the specific language governing permissions and 13 : // limitations under the License. 14 : 15 : package com.google.gerrit.sshd; 16 : 17 : import com.google.common.base.Strings; 18 : import com.google.common.base.Throwables; 19 : import com.google.common.collect.Sets; 20 : import com.google.common.util.concurrent.Atomics; 21 : import com.google.gerrit.extensions.registration.DynamicSet; 22 : import com.google.gerrit.extensions.restapi.AuthException; 23 : import com.google.gerrit.server.DynamicOptions; 24 : import com.google.gerrit.server.args4j.SubcommandHandler; 25 : import com.google.gerrit.server.permissions.GlobalPermission; 26 : import com.google.gerrit.server.permissions.PermissionBackend; 27 : import com.google.gerrit.server.permissions.PermissionBackendException; 28 : import com.google.inject.Inject; 29 : import com.google.inject.assistedinject.Assisted; 30 : import java.io.IOException; 31 : import java.io.StringWriter; 32 : import java.util.ArrayList; 33 : import java.util.List; 34 : import java.util.Map; 35 : import java.util.concurrent.atomic.AtomicReference; 36 : import org.apache.sshd.server.Environment; 37 : import org.apache.sshd.server.channel.ChannelSession; 38 : import org.apache.sshd.server.command.Command; 39 : import org.kohsuke.args4j.Argument; 40 : 41 : /** Command that dispatches to a subcommand from its command table. */ 42 : final class DispatchCommand extends BaseCommand { 43 : interface Factory { 44 : DispatchCommand create(Map<String, CommandProvider> map); 45 : } 46 : 47 : private final PermissionBackend permissionBackend; 48 : private final Map<String, CommandProvider> commands; 49 : private final AtomicReference<Command> atomicCmd; 50 : private final DynamicSet<SshExecuteCommandInterceptor> commandInterceptors; 51 : 52 : @Argument(index = 0, required = false, metaVar = "COMMAND", handler = SubcommandHandler.class) 53 : private String commandName; 54 : 55 9 : @Argument(index = 1, multiValued = true, metaVar = "ARG") 56 : private List<String> args = new ArrayList<>(); 57 : 58 : @Inject 59 : DispatchCommand( 60 : PermissionBackend permissionBackend, 61 : DynamicSet<SshExecuteCommandInterceptor> commandInterceptors, 62 9 : @Assisted Map<String, CommandProvider> all) { 63 9 : this.permissionBackend = permissionBackend; 64 9 : commands = all; 65 9 : atomicCmd = Atomics.newReference(); 66 9 : this.commandInterceptors = commandInterceptors; 67 9 : } 68 : 69 : Map<String, CommandProvider> getMap() { 70 0 : return commands; 71 : } 72 : 73 : @Override 74 : public void start(ChannelSession channel, Environment env) throws IOException { 75 9 : try (DynamicOptions pluginOptions = new DynamicOptions(injector, dynamicBeans)) { 76 9 : parseCommandLine(pluginOptions); 77 9 : if (Strings.isNullOrEmpty(commandName)) { 78 0 : StringWriter msg = new StringWriter(); 79 0 : msg.write(usage()); 80 0 : throw die(msg.toString()); 81 : } 82 : 83 9 : final CommandProvider p = commands.get(commandName); 84 9 : if (p == null) { 85 : String msg = 86 1 : (getName().isEmpty() ? "Gerrit Code Review" : getName()) 87 : + ": " 88 : + commandName 89 : + ": not found"; 90 1 : throw die(msg); 91 : } 92 : 93 9 : final Command cmd = p.getProvider().get(); 94 9 : checkRequiresCapability(cmd); 95 9 : String actualCommandName = commandName; 96 9 : if (cmd instanceof BaseCommand) { 97 9 : final BaseCommand bc = (BaseCommand) cmd; 98 9 : if (!getName().isEmpty()) { 99 5 : actualCommandName = getName() + " " + commandName; 100 : } 101 9 : bc.setName(actualCommandName); 102 9 : bc.setArguments(args.toArray(new String[args.size()])); 103 : 104 9 : } else if (!args.isEmpty()) { 105 0 : throw die(commandName + " does not take arguments"); 106 : } 107 : 108 9 : for (SshExecuteCommandInterceptor commandInterceptor : commandInterceptors) { 109 0 : if (!commandInterceptor.accept(actualCommandName, args)) { 110 0 : throw new UnloggedFailure( 111 : 126, 112 0 : String.format( 113 : "blocked by %s, contact gerrit administrators for more details", 114 0 : commandInterceptor.name())); 115 : } 116 0 : } 117 : 118 9 : provideStateTo(cmd); 119 9 : atomicCmd.set(cmd); 120 9 : cmd.start(channel, env); 121 : 122 1 : } catch (UnloggedFailure e) { 123 1 : String msg = e.getMessage(); 124 1 : if (!msg.endsWith("\n")) { 125 1 : msg += "\n"; 126 : } 127 1 : err.write(msg.getBytes(ENC)); 128 1 : err.flush(); 129 1 : onExit(e.exitCode); 130 9 : } 131 9 : } 132 : 133 : private void checkRequiresCapability(Command cmd) throws UnloggedFailure { 134 9 : String pluginName = null; 135 9 : if (cmd instanceof BaseCommand) { 136 9 : pluginName = ((BaseCommand) cmd).getPluginName(); 137 : } 138 : try { 139 9 : permissionBackend 140 9 : .currentUser() 141 9 : .checkAny(GlobalPermission.fromAnnotation(pluginName, cmd.getClass())); 142 1 : } catch (AuthException e) { 143 1 : throw new UnloggedFailure(BaseCommand.STATUS_NOT_ADMIN, e.getMessage()); 144 0 : } catch (PermissionBackendException e) { 145 0 : throw new UnloggedFailure(1, "fatal: permission check unavailable", e); 146 9 : } 147 9 : } 148 : 149 : @Override 150 : public void destroy(ChannelSession channel) { 151 9 : Command cmd = atomicCmd.getAndSet(null); 152 9 : if (cmd != null) { 153 : try { 154 9 : cmd.destroy(channel); 155 0 : } catch (Exception e) { 156 0 : Throwables.throwIfUnchecked(e); 157 0 : throw new RuntimeException(e); 158 9 : } 159 : } 160 9 : } 161 : 162 : @Override 163 : protected String usage() { 164 1 : final StringBuilder usage = new StringBuilder(); 165 1 : usage.append("Available commands"); 166 1 : if (!getName().isEmpty()) { 167 1 : usage.append(" of "); 168 1 : usage.append(getName()); 169 : } 170 1 : usage.append(" are:\n"); 171 1 : usage.append("\n"); 172 : 173 1 : int maxLength = -1; 174 1 : for (String name : commands.keySet()) { 175 1 : maxLength = Math.max(maxLength, name.length()); 176 1 : } 177 1 : String format = "%-" + maxLength + "s %s"; 178 1 : for (String name : Sets.newTreeSet(commands.keySet())) { 179 1 : final CommandProvider p = commands.get(name); 180 1 : usage.append(" "); 181 1 : usage.append(String.format(format, name, Strings.nullToEmpty(p.getDescription()))); 182 1 : usage.append("\n"); 183 1 : } 184 1 : usage.append("\n"); 185 : 186 1 : usage.append("See '"); 187 1 : if (getName().indexOf(' ') < 0) { 188 1 : usage.append(getName()); 189 1 : usage.append(' '); 190 : } 191 1 : usage.append("COMMAND --help' for more information.\n"); 192 1 : usage.append("\n"); 193 1 : return usage.toString(); 194 : } 195 : 196 : public String getCommandName() { 197 9 : return commandName; 198 : } 199 : }