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.flogger.FluentLogger; 18 : import com.google.common.util.concurrent.Atomics; 19 : import com.google.common.util.concurrent.ThreadFactoryBuilder; 20 : import com.google.gerrit.extensions.events.LifecycleListener; 21 : import com.google.gerrit.extensions.registration.DynamicItem; 22 : import com.google.gerrit.server.config.GerritServerConfig; 23 : import com.google.gerrit.server.git.WorkQueue; 24 : import com.google.gerrit.server.logging.LoggingContextAwareExecutorService; 25 : import com.google.gerrit.sshd.SshScope.Context; 26 : import com.google.inject.Inject; 27 : import com.google.inject.Provider; 28 : import com.google.inject.Singleton; 29 : import java.io.IOException; 30 : import java.io.InputStream; 31 : import java.io.OutputStream; 32 : import java.util.ArrayList; 33 : import java.util.List; 34 : import java.util.concurrent.ExecutorService; 35 : import java.util.concurrent.Executors; 36 : import java.util.concurrent.Future; 37 : import java.util.concurrent.ScheduledExecutorService; 38 : import java.util.concurrent.atomic.AtomicBoolean; 39 : import java.util.concurrent.atomic.AtomicReference; 40 : import org.apache.sshd.server.Environment; 41 : import org.apache.sshd.server.ExitCallback; 42 : import org.apache.sshd.server.channel.ChannelSession; 43 : import org.apache.sshd.server.command.Command; 44 : import org.apache.sshd.server.command.CommandFactory; 45 : import org.apache.sshd.server.session.ServerSession; 46 : import org.apache.sshd.server.session.ServerSessionAware; 47 : import org.eclipse.jgit.lib.Config; 48 : 49 : /** Creates a CommandFactory using commands registered by {@link CommandModule}. */ 50 : @Singleton 51 : class CommandFactoryProvider implements Provider<CommandFactory>, LifecycleListener { 52 17 : private static final FluentLogger logger = FluentLogger.forEnclosingClass(); 53 : 54 : private final DispatchCommandProvider dispatcher; 55 : private final SshLog log; 56 : private final SshScope sshScope; 57 : private final ScheduledExecutorService startExecutor; 58 : private final ExecutorService destroyExecutor; 59 : private final DynamicItem<SshCreateCommandInterceptor> createCommandInterceptor; 60 : 61 : @Inject 62 : CommandFactoryProvider( 63 : @CommandName(Commands.ROOT) DispatchCommandProvider d, 64 : @GerritServerConfig Config cfg, 65 : WorkQueue workQueue, 66 : SshLog l, 67 : SshScope s, 68 17 : DynamicItem<SshCreateCommandInterceptor> i) { 69 17 : dispatcher = d; 70 17 : log = l; 71 17 : sshScope = s; 72 17 : createCommandInterceptor = i; 73 : 74 17 : int threads = cfg.getInt("sshd", "commandStartThreads", 2); 75 17 : startExecutor = workQueue.createQueue(threads, "SshCommandStart", true); 76 17 : destroyExecutor = 77 : new LoggingContextAwareExecutorService( 78 17 : Executors.newSingleThreadExecutor( 79 : new ThreadFactoryBuilder() 80 17 : .setNameFormat("SshCommandDestroy-%s") 81 17 : .setDaemon(true) 82 17 : .build())); 83 17 : } 84 : 85 : @Override 86 17 : public void start() {} 87 : 88 : @Override 89 : public void stop() { 90 17 : destroyExecutor.shutdownNow(); 91 17 : } 92 : 93 : @Override 94 : public CommandFactory get() { 95 17 : return (channelSession, requestCommand) -> { 96 9 : String command = requestCommand; 97 9 : SshCreateCommandInterceptor interceptor = createCommandInterceptor.get(); 98 9 : if (interceptor != null) { 99 0 : command = interceptor.intercept(command); 100 : } 101 9 : return new Trampoline(command); 102 : }; 103 : } 104 : 105 : private class Trampoline implements Command, ServerSessionAware { 106 : private final String commandLine; 107 : private final String[] argv; 108 : private InputStream in; 109 : private OutputStream out; 110 : private OutputStream err; 111 : private ExitCallback exit; 112 : private Environment env; 113 : private Context ctx; 114 : private DispatchCommand cmd; 115 : private final AtomicBoolean logged; 116 : private final AtomicReference<Future<?>> task; 117 : 118 9 : Trampoline(String cmdLine) { 119 9 : commandLine = cmdLine; 120 9 : argv = split(cmdLine); 121 9 : logged = new AtomicBoolean(); 122 9 : task = Atomics.newReference(); 123 9 : } 124 : 125 : @Override 126 : public void setInputStream(InputStream in) { 127 9 : this.in = in; 128 9 : } 129 : 130 : @Override 131 : public void setOutputStream(OutputStream out) { 132 9 : this.out = out; 133 9 : } 134 : 135 : @Override 136 : public void setErrorStream(OutputStream err) { 137 9 : this.err = err; 138 9 : } 139 : 140 : @Override 141 : public void setExitCallback(ExitCallback callback) { 142 9 : this.exit = callback; 143 9 : } 144 : 145 : @Override 146 : public void setSession(ServerSession session) { 147 9 : final SshSession s = session.getAttribute(SshSession.KEY); 148 9 : this.ctx = sshScope.newContext(s, commandLine); 149 9 : } 150 : 151 : @Override 152 : public void start(ChannelSession channel, Environment env) throws IOException { 153 9 : this.env = env; 154 9 : final Context ctx = this.ctx; 155 9 : task.set( 156 9 : startExecutor.submit( 157 9 : new Runnable() { 158 : @Override 159 : public void run() { 160 : try { 161 9 : onStart(channel); 162 0 : } catch (Exception e) { 163 0 : logger.atWarning().withCause(e).log( 164 : "Cannot start command \"%s\" for user %s", 165 0 : ctx.getCommandLine(), ctx.getSession().getUsername()); 166 9 : } 167 9 : } 168 : 169 : @Override 170 : public String toString() { 171 0 : return "start (user " + ctx.getSession().getUsername() + ")"; 172 : } 173 : })); 174 9 : } 175 : 176 : private void onStart(ChannelSession channel) throws IOException { 177 9 : synchronized (this) { 178 9 : final Context old = sshScope.set(ctx); 179 : try { 180 9 : cmd = dispatcher.get(); 181 9 : cmd.setArguments(argv); 182 9 : cmd.setInputStream(in); 183 9 : cmd.setOutputStream(out); 184 9 : cmd.setErrorStream(err); 185 9 : cmd.setExitCallback( 186 9 : new ExitCallback() { 187 : @Override 188 : public void onExit(int rc, String exitMessage, boolean closeImmediately) { 189 0 : exit.onExit(translateExit(rc), exitMessage, closeImmediately); 190 0 : log(rc, exitMessage); 191 0 : } 192 : 193 : @Override 194 : public void onExit(int rc, String exitMessage) { 195 3 : exit.onExit(translateExit(rc), exitMessage); 196 3 : log(rc, exitMessage); 197 3 : } 198 : 199 : @Override 200 : public void onExit(int rc) { 201 8 : exit.onExit(translateExit(rc)); 202 8 : log(rc); 203 8 : } 204 : }); 205 9 : cmd.start(channel, env); 206 : } finally { 207 9 : sshScope.set(old); 208 : } 209 9 : } 210 9 : } 211 : 212 : private int translateExit(int rc) { 213 9 : switch (rc) { 214 : case BaseCommand.STATUS_NOT_ADMIN: 215 1 : return 1; 216 : 217 : case BaseCommand.STATUS_CANCEL: 218 0 : return 15 /* SIGKILL */; 219 : 220 : case BaseCommand.STATUS_NOT_FOUND: 221 0 : return 127 /* POSIX not found */; 222 : 223 : default: 224 9 : return rc; 225 : } 226 : } 227 : 228 : private void log(int rc) { 229 9 : if (logged.compareAndSet(false, true)) { 230 9 : log.onExecute(cmd, rc, ctx.getSession()); 231 : } 232 9 : } 233 : 234 : private void log(int rc, String message) { 235 3 : if (logged.compareAndSet(false, true)) { 236 3 : log.onExecute(cmd, rc, ctx.getSession(), message); 237 : } 238 3 : } 239 : 240 : @Override 241 : public void destroy(ChannelSession channel) { 242 9 : Future<?> future = task.getAndSet(null); 243 9 : if (future != null) { 244 9 : future.cancel(true); 245 9 : destroyExecutor.execute(() -> onDestroy(channel)); 246 : } 247 9 : } 248 : 249 : private void onDestroy(ChannelSession channel) { 250 9 : synchronized (this) { 251 9 : if (cmd != null) { 252 9 : final Context old = sshScope.set(ctx); 253 : try { 254 9 : cmd.destroy(channel); 255 9 : log(BaseCommand.STATUS_CANCEL); 256 : } finally { 257 9 : ctx = null; 258 9 : cmd = null; 259 9 : sshScope.set(old); 260 : } 261 : } 262 9 : } 263 9 : } 264 : } 265 : 266 : /** Split a command line into a string array. */ 267 : public static String[] split(String commandLine) { 268 9 : final List<String> list = new ArrayList<>(); 269 9 : boolean inquote = false; 270 9 : boolean inDblQuote = false; 271 9 : StringBuilder r = new StringBuilder(); 272 9 : for (int ip = 0; ip < commandLine.length(); ) { 273 9 : final char b = commandLine.charAt(ip++); 274 9 : switch (b) { 275 : case '\t': 276 : case ' ': 277 9 : if (inquote || inDblQuote) { 278 1 : r.append(b); 279 9 : } else if (r.length() > 0) { 280 9 : list.add(r.toString()); 281 9 : r = new StringBuilder(); 282 : } 283 : continue; 284 : case '\"': 285 1 : if (inquote) { 286 0 : r.append(b); 287 : } else { 288 1 : inDblQuote = !inDblQuote; 289 : } 290 1 : continue; 291 : case '\'': 292 5 : if (inDblQuote) { 293 0 : r.append(b); 294 : } else { 295 5 : inquote = !inquote; 296 : } 297 5 : continue; 298 : case '\\': 299 0 : if (inquote || ip == commandLine.length()) { 300 0 : r.append(b); // literal within a quote 301 : } else { 302 0 : r.append(commandLine.charAt(ip++)); 303 : } 304 0 : continue; 305 : default: 306 9 : r.append(b); 307 9 : continue; 308 : } 309 : } 310 9 : if (r.length() > 0) { 311 9 : list.add(r.toString()); 312 : } 313 9 : return list.toArray(new String[list.size()]); 314 : } 315 : }