| // 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.flogger.FluentLogger; | |
| import com.google.common.util.concurrent.Atomics; | |
| import com.google.common.util.concurrent.ThreadFactoryBuilder; | |
| import com.google.gerrit.extensions.events.LifecycleListener; | |
| import com.google.gerrit.extensions.registration.DynamicItem; | |
| import com.google.gerrit.server.config.GerritServerConfig; | |
| import com.google.gerrit.server.git.WorkQueue; | |
| import com.google.gerrit.server.logging.LoggingContextAwareExecutorService; | |
| import com.google.gerrit.sshd.SshScope.Context; | |
| import com.google.inject.Inject; | |
| import com.google.inject.Provider; | |
| import com.google.inject.Singleton; | |
| import java.io.IOException; | |
| import java.io.InputStream; | |
| import java.io.OutputStream; | |
| import java.util.ArrayList; | |
| import java.util.List; | |
| import java.util.concurrent.ExecutorService; | |
| import java.util.concurrent.Executors; | |
| import java.util.concurrent.Future; | |
| import java.util.concurrent.ScheduledExecutorService; | |
| import java.util.concurrent.atomic.AtomicBoolean; | |
| import java.util.concurrent.atomic.AtomicReference; | |
| import org.apache.sshd.server.Environment; | |
| import org.apache.sshd.server.ExitCallback; | |
| import org.apache.sshd.server.channel.ChannelSession; | |
| import org.apache.sshd.server.command.Command; | |
| import org.apache.sshd.server.command.CommandFactory; | |
| import org.apache.sshd.server.session.ServerSession; | |
| import org.apache.sshd.server.session.ServerSessionAware; | |
| import org.eclipse.jgit.lib.Config; | |
| /** Creates a CommandFactory using commands registered by {@link CommandModule}. */ | |
| @Singleton | |
| class CommandFactoryProvider implements Provider<CommandFactory>, LifecycleListener { | |
| private static final FluentLogger logger = FluentLogger.forEnclosingClass(); | |
| private final DispatchCommandProvider dispatcher; | |
| private final SshLog log; | |
| private final SshScope sshScope; | |
| private final ScheduledExecutorService startExecutor; | |
| private final ExecutorService destroyExecutor; | |
| private final DynamicItem<SshCreateCommandInterceptor> createCommandInterceptor; | |
| @Inject | |
| CommandFactoryProvider( | |
| @CommandName(Commands.ROOT) DispatchCommandProvider d, | |
| @GerritServerConfig Config cfg, | |
| WorkQueue workQueue, | |
| SshLog l, | |
| SshScope s, | |
| DynamicItem<SshCreateCommandInterceptor> i) { | |
| dispatcher = d; | |
| log = l; | |
| sshScope = s; | |
| createCommandInterceptor = i; | |
| int threads = cfg.getInt("sshd", "commandStartThreads", 2); | |
| startExecutor = workQueue.createQueue(threads, "SshCommandStart", true); | |
| destroyExecutor = | |
| new LoggingContextAwareExecutorService( | |
| Executors.newSingleThreadExecutor( | |
| new ThreadFactoryBuilder() | |
| .setNameFormat("SshCommandDestroy-%s") | |
| .setDaemon(true) | |
| .build())); | |
| } | |
| @Override | |
| public void start() {} | |
| @Override | |
| public void stop() { | |
| destroyExecutor.shutdownNow(); | |
| } | |
| @Override | |
| public CommandFactory get() { | |
| return (channelSession, requestCommand) -> { | |
| String command = requestCommand; | |
| SshCreateCommandInterceptor interceptor = createCommandInterceptor.get(); | |
| if (interceptor != null) { | |
| command = interceptor.intercept(command); | |
| } | |
| return new Trampoline(command); | |
| }; | |
| } | |
| private class Trampoline implements Command, ServerSessionAware { | |
| private final String commandLine; | |
| private final String[] argv; | |
| private InputStream in; | |
| private OutputStream out; | |
| private OutputStream err; | |
| private ExitCallback exit; | |
| private Environment env; | |
| private Context ctx; | |
| private DispatchCommand cmd; | |
| private final AtomicBoolean logged; | |
| private final AtomicReference<Future<?>> task; | |
| Trampoline(String cmdLine) { | |
| commandLine = cmdLine; | |
| argv = split(cmdLine); | |
| logged = new AtomicBoolean(); | |
| task = Atomics.newReference(); | |
| } | |
| @Override | |
| public void setInputStream(InputStream in) { | |
| this.in = in; | |
| } | |
| @Override | |
| public void setOutputStream(OutputStream out) { | |
| this.out = out; | |
| } | |
| @Override | |
| public void setErrorStream(OutputStream err) { | |
| this.err = err; | |
| } | |
| @Override | |
| public void setExitCallback(ExitCallback callback) { | |
| this.exit = callback; | |
| } | |
| @Override | |
| public void setSession(ServerSession session) { | |
| final SshSession s = session.getAttribute(SshSession.KEY); | |
| this.ctx = sshScope.newContext(s, commandLine); | |
| } | |
| @Override | |
| public void start(ChannelSession channel, Environment env) throws IOException { | |
| this.env = env; | |
| final Context ctx = this.ctx; | |
| task.set( | |
| startExecutor.submit( | |
| new Runnable() { | |
| @Override | |
| public void run() { | |
| try { | |
| onStart(channel); | |
| } catch (Exception e) { | |
| logger.atWarning().withCause(e).log( | |
| "Cannot start command \"%s\" for user %s", | |
| ctx.getCommandLine(), ctx.getSession().getUsername()); | |
| } | |
| } | |
| @Override | |
| public String toString() { | |
| return "start (user " + ctx.getSession().getUsername() + ")"; | |
| } | |
| })); | |
| } | |
| private void onStart(ChannelSession channel) throws IOException { | |
| synchronized (this) { | |
| final Context old = sshScope.set(ctx); | |
| try { | |
| cmd = dispatcher.get(); | |
| cmd.setArguments(argv); | |
| cmd.setInputStream(in); | |
| cmd.setOutputStream(out); | |
| cmd.setErrorStream(err); | |
| cmd.setExitCallback( | |
| new ExitCallback() { | |
| @Override | |
| public void onExit(int rc, String exitMessage, boolean closeImmediately) { | |
| exit.onExit(translateExit(rc), exitMessage, closeImmediately); | |
| log(rc, exitMessage); | |
| } | |
| @Override | |
| public void onExit(int rc, String exitMessage) { | |
| exit.onExit(translateExit(rc), exitMessage); | |
| log(rc, exitMessage); | |
| } | |
| @Override | |
| public void onExit(int rc) { | |
| exit.onExit(translateExit(rc)); | |
| log(rc); | |
| } | |
| }); | |
| cmd.start(channel, env); | |
| } finally { | |
| sshScope.set(old); | |
| } | |
| } | |
| } | |
| private int translateExit(int rc) { | |
| switch (rc) { | |
| case BaseCommand.STATUS_NOT_ADMIN: | |
| return 1; | |
| case BaseCommand.STATUS_CANCEL: | |
| return 15 /* SIGKILL */; | |
| case BaseCommand.STATUS_NOT_FOUND: | |
| return 127 /* POSIX not found */; | |
| default: | |
| return rc; | |
| } | |
| } | |
| private void log(int rc) { | |
| if (logged.compareAndSet(false, true)) { | |
| log.onExecute(cmd, rc, ctx.getSession()); | |
| } | |
| } | |
| private void log(int rc, String message) { | |
| if (logged.compareAndSet(false, true)) { | |
| log.onExecute(cmd, rc, ctx.getSession(), message); | |
| } | |
| } | |
| @Override | |
| public void destroy(ChannelSession channel) { | |
| Future<?> future = task.getAndSet(null); | |
| if (future != null) { | |
| future.cancel(true); | |
| destroyExecutor.execute(() -> onDestroy(channel)); | |
| } | |
| } | |
| private void onDestroy(ChannelSession channel) { | |
| synchronized (this) { | |
| if (cmd != null) { | |
| final Context old = sshScope.set(ctx); | |
| try { | |
| cmd.destroy(channel); | |
| log(BaseCommand.STATUS_CANCEL); | |
| } finally { | |
| ctx = null; | |
| cmd = null; | |
| sshScope.set(old); | |
| } | |
| } | |
| } | |
| } | |
| } | |
| /** Split a command line into a string array. */ | |
| public static String[] split(String commandLine) { | |
| final List<String> list = new ArrayList<>(); | |
| boolean inquote = false; | |
| boolean inDblQuote = 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 || inDblQuote) { | |
| r.append(b); | |
| } else if (r.length() > 0) { | |
| list.add(r.toString()); | |
| r = new StringBuilder(); | |
| } | |
| continue; | |
| case '\"': | |
| if (inquote) { | |
| r.append(b); | |
| } else { | |
| inDblQuote = !inDblQuote; | |
| } | |
| continue; | |
| case '\'': | |
| if (inDblQuote) { | |
| r.append(b); | |
| } else { | |
| 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()]); | |
| } | |
| } |