LCOV - code coverage report
Current view: top level - sshd - CommandFactoryProvider.java (source / functions) Hit Total Coverage
Test: _coverage_report.dat Lines: 114 130 87.7 %
Date: 2022-11-19 15:00:39 Functions: 26 28 92.9 %

          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             : }

Generated by: LCOV version 1.16+git.20220603.dfeb750