[ Index ]

PHP Cross Reference of Phabricator

title

Body

[close]

/src/infrastructure/ssh/ -> PhabricatorSSHPassthruCommand.php (source)

   1  <?php
   2  
   3  /**
   4   * Proxy an IO channel to an underlying command, with optional callbacks. This
   5   * is a mostly a more general version of @{class:PhutilExecPassthru}. This
   6   * class is used to proxy Git, SVN and Mercurial traffic to the commands which
   7   * can actually serve it.
   8   *
   9   * Largely, this just reads an IO channel (like stdin from SSH) and writes
  10   * the results into a command channel (like a command's stdin). Then it reads
  11   * the command channel (like the command's stdout) and writes it into the IO
  12   * channel (like stdout from SSH):
  13   *
  14   *    IO Channel        Command Channel
  15   *    stdin       ->    stdin
  16   *    stdout      <-    stdout
  17   *    stderr      <-    stderr
  18   *
  19   * You can provide **read and write callbacks** which are invoked as data
  20   * is passed through this class. They allow you to inspect and modify traffic.
  21   *
  22   *    IO Channel     Passthru        Command Channel
  23   *    stdout     ->  willWrite   ->  stdin
  24   *    stdin      <-  willRead    <-  stdout
  25   *    stderr     <-  (identity)  <-  stderr
  26   *
  27   * Primarily, this means:
  28   *
  29   *   - the **IO Channel** can be a @{class:PhutilProtocolChannel} if the
  30   *     **write callback** can convert protocol messages into strings; and
  31   *   - the **write callback** can inspect and reject requests over the channel,
  32   *     e.g. to enforce policies.
  33   *
  34   * In practice, this is used when serving repositories to check each command
  35   * issued over SSH and determine if it is a read command or a write command.
  36   * Writes can then be checked for appropriate permissions.
  37   */
  38  final class PhabricatorSSHPassthruCommand extends Phobject {
  39  
  40    private $commandChannel;
  41    private $ioChannel;
  42    private $errorChannel;
  43    private $execFuture;
  44    private $willWriteCallback;
  45    private $willReadCallback;
  46  
  47    public function setCommandChannelFromExecFuture(ExecFuture $exec_future) {
  48      $exec_channel = new PhutilExecChannel($exec_future);
  49      $exec_channel->setStderrHandler(array($this, 'writeErrorIOCallback'));
  50  
  51      $this->execFuture = $exec_future;
  52      $this->commandChannel = $exec_channel;
  53  
  54      return $this;
  55    }
  56  
  57    public function setIOChannel(PhutilChannel $io_channel) {
  58      $this->ioChannel = $io_channel;
  59      return $this;
  60    }
  61  
  62    public function setErrorChannel(PhutilChannel $error_channel) {
  63      $this->errorChannel = $error_channel;
  64      return $this;
  65    }
  66  
  67    public function setWillReadCallback($will_read_callback) {
  68      $this->willReadCallback = $will_read_callback;
  69      return $this;
  70    }
  71  
  72    public function setWillWriteCallback($will_write_callback) {
  73      $this->willWriteCallback = $will_write_callback;
  74      return $this;
  75    }
  76  
  77    public function writeErrorIOCallback(PhutilChannel $channel, $data) {
  78      $this->errorChannel->write($data);
  79    }
  80  
  81    public function execute() {
  82      $command_channel = $this->commandChannel;
  83      $io_channel = $this->ioChannel;
  84      $error_channel = $this->errorChannel;
  85  
  86      if (!$command_channel) {
  87        throw new Exception('Set a command channel before calling execute()!');
  88      }
  89  
  90      if (!$io_channel) {
  91        throw new Exception('Set an IO channel before calling execute()!');
  92      }
  93  
  94      if (!$error_channel) {
  95        throw new Exception('Set an error channel before calling execute()!');
  96      }
  97  
  98      $channels = array($command_channel, $io_channel, $error_channel);
  99  
 100      // We want to limit the amount of data we'll hold in memory for this
 101      // process. See T4241 for a discussion of this issue in general.
 102  
 103      $buffer_size = (1024 * 1024); // 1MB
 104      $io_channel->setReadBufferSize($buffer_size);
 105      $command_channel->setReadBufferSize($buffer_size);
 106  
 107      // TODO: This just makes us throw away stderr after the first 1MB, but we
 108      // don't currently have the support infrastructure to buffer it correctly.
 109      // It's difficult to imagine this causing problems in practice, though.
 110      $this->execFuture->getStderrSizeLimit($buffer_size);
 111  
 112      while (true) {
 113        PhutilChannel::waitForAny($channels);
 114  
 115        $io_channel->update();
 116        $command_channel->update();
 117        $error_channel->update();
 118  
 119        // If any channel is blocked on the other end, wait for it to flush before
 120        // we continue reading. For example, if a user is running `git clone` on
 121        // a 1GB repository, the underlying `git-upload-pack` may
 122        // be able to produce data much more quickly than we can send it over
 123        // the network. If we don't throttle the reads, we may only send a few
 124        // MB over the I/O channel in the time it takes to read the entire 1GB off
 125        // the command channel. That leaves us with 1GB of data in memory.
 126  
 127        while ($command_channel->isOpen() &&
 128               $io_channel->isOpenForWriting() &&
 129               ($command_channel->getWriteBufferSize() >= $buffer_size ||
 130               $io_channel->getWriteBufferSize() >= $buffer_size ||
 131               $error_channel->getWriteBufferSize() >= $buffer_size)) {
 132          PhutilChannel::waitForActivity(array(), $channels);
 133          $io_channel->update();
 134          $command_channel->update();
 135          $error_channel->update();
 136        }
 137  
 138        // If the subprocess has exited and we've read everything from it,
 139        // we're all done.
 140        $done = !$command_channel->isOpenForReading() &&
 141                 $command_channel->isReadBufferEmpty();
 142  
 143        $in_message = $io_channel->read();
 144        if ($in_message !== null) {
 145          $in_message = $this->willWriteData($in_message);
 146          if ($in_message !== null) {
 147            $command_channel->write($in_message);
 148          }
 149        }
 150  
 151        $out_message = $command_channel->read();
 152        if ($out_message !== null) {
 153          $out_message = $this->willReadData($out_message);
 154          if ($out_message !== null) {
 155            $io_channel->write($out_message);
 156          }
 157        }
 158  
 159        // If we have nothing left on stdin, close stdin on the subprocess.
 160        if (!$io_channel->isOpenForReading()) {
 161          $command_channel->closeWriteChannel();
 162        }
 163  
 164        if ($done) {
 165          break;
 166        }
 167  
 168        // If the client has disconnected, kill the subprocess and bail.
 169        if (!$io_channel->isOpenForWriting()) {
 170          $this->execFuture
 171            ->setStdoutSizeLimit(0)
 172            ->setStderrSizeLimit(0)
 173            ->setReadBufferSize(null)
 174            ->resolveKill();
 175          break;
 176        }
 177      }
 178  
 179      list($err) = $this->execFuture
 180        ->setStdoutSizeLimit(0)
 181        ->setStderrSizeLimit(0)
 182        ->setReadBufferSize(null)
 183        ->resolve();
 184  
 185      return $err;
 186    }
 187  
 188    public function willWriteData($message) {
 189      if ($this->willWriteCallback) {
 190        return call_user_func($this->willWriteCallback, $this, $message);
 191      } else {
 192        if (strlen($message)) {
 193          return $message;
 194        } else {
 195          return null;
 196        }
 197      }
 198    }
 199  
 200    public function willReadData($message) {
 201      if ($this->willReadCallback) {
 202        return call_user_func($this->willReadCallback, $this, $message);
 203      } else {
 204        if (strlen($message)) {
 205          return $message;
 206        } else {
 207          return null;
 208        }
 209      }
 210    }
 211  
 212  }


Generated: Sun Nov 30 09:20:46 2014 Cross-referenced by PHPXref 0.7.1