Path: blob/master/src/infrastructure/ssh/PhabricatorSSHPassthruCommand.php
12241 views
<?php12/**3* Proxy an IO channel to an underlying command, with optional callbacks. This4* is a mostly a more general version of @{class:PhutilExecPassthru}. This5* class is used to proxy Git, SVN and Mercurial traffic to the commands which6* can actually serve it.7*8* Largely, this just reads an IO channel (like stdin from SSH) and writes9* the results into a command channel (like a command's stdin). Then it reads10* the command channel (like the command's stdout) and writes it into the IO11* channel (like stdout from SSH):12*13* IO Channel Command Channel14* stdin -> stdin15* stdout <- stdout16* stderr <- stderr17*18* You can provide **read and write callbacks** which are invoked as data19* is passed through this class. They allow you to inspect and modify traffic.20*21* IO Channel Passthru Command Channel22* stdout -> willWrite -> stdin23* stdin <- willRead <- stdout24* stderr <- (identity) <- stderr25*26* Primarily, this means:27*28* - the **IO Channel** can be a @{class:PhutilProtocolChannel} if the29* **write callback** can convert protocol messages into strings; and30* - the **write callback** can inspect and reject requests over the channel,31* e.g. to enforce policies.32*33* In practice, this is used when serving repositories to check each command34* issued over SSH and determine if it is a read command or a write command.35* Writes can then be checked for appropriate permissions.36*/37final class PhabricatorSSHPassthruCommand extends Phobject {3839private $commandChannel;40private $ioChannel;41private $errorChannel;42private $execFuture;43private $willWriteCallback;44private $willReadCallback;45private $pauseIOReads;4647public function setCommandChannelFromExecFuture(ExecFuture $exec_future) {48$exec_channel = new PhutilExecChannel($exec_future);49$exec_channel->setStderrHandler(array($this, 'writeErrorIOCallback'));5051$this->execFuture = $exec_future;52$this->commandChannel = $exec_channel;5354return $this;55}5657public function setIOChannel(PhutilChannel $io_channel) {58$this->ioChannel = $io_channel;59return $this;60}6162public function setErrorChannel(PhutilChannel $error_channel) {63$this->errorChannel = $error_channel;64return $this;65}6667public function setWillReadCallback($will_read_callback) {68$this->willReadCallback = $will_read_callback;69return $this;70}7172public function setWillWriteCallback($will_write_callback) {73$this->willWriteCallback = $will_write_callback;74return $this;75}7677public function writeErrorIOCallback(PhutilChannel $channel, $data) {78$this->errorChannel->write($data);79}8081public function setPauseIOReads($pause) {82$this->pauseIOReads = $pause;83return $this;84}8586public function execute() {87$command_channel = $this->commandChannel;88$io_channel = $this->ioChannel;89$error_channel = $this->errorChannel;9091if (!$command_channel) {92throw new Exception(93pht(94'Set a command channel before calling %s!',95__FUNCTION__.'()'));96}9798if (!$io_channel) {99throw new Exception(100pht(101'Set an IO channel before calling %s!',102__FUNCTION__.'()'));103}104105if (!$error_channel) {106throw new Exception(107pht(108'Set an error channel before calling %s!',109__FUNCTION__.'()'));110}111112$channels = array($command_channel, $io_channel, $error_channel);113114// We want to limit the amount of data we'll hold in memory for this115// process. See T4241 for a discussion of this issue in general.116117$buffer_size = (1024 * 1024); // 1MB118$io_channel->setReadBufferSize($buffer_size);119$command_channel->setReadBufferSize($buffer_size);120121// TODO: This just makes us throw away stderr after the first 1MB, but we122// don't currently have the support infrastructure to buffer it correctly.123// It's difficult to imagine this causing problems in practice, though.124$this->execFuture->getStderrSizeLimit($buffer_size);125126while (true) {127PhutilChannel::waitForAny($channels);128129$io_channel->update();130$command_channel->update();131$error_channel->update();132133// If any channel is blocked on the other end, wait for it to flush before134// we continue reading. For example, if a user is running `git clone` on135// a 1GB repository, the underlying `git-upload-pack` may136// be able to produce data much more quickly than we can send it over137// the network. If we don't throttle the reads, we may only send a few138// MB over the I/O channel in the time it takes to read the entire 1GB off139// the command channel. That leaves us with 1GB of data in memory.140141while ($command_channel->isOpen() &&142$io_channel->isOpenForWriting() &&143($command_channel->getWriteBufferSize() >= $buffer_size ||144$io_channel->getWriteBufferSize() >= $buffer_size ||145$error_channel->getWriteBufferSize() >= $buffer_size)) {146PhutilChannel::waitForActivity(array(), $channels);147$io_channel->update();148$command_channel->update();149$error_channel->update();150}151152// If the subprocess has exited and we've read everything from it,153// we're all done.154$done = !$command_channel->isOpenForReading() &&155$command_channel->isReadBufferEmpty();156157if (!$this->pauseIOReads) {158$in_message = $io_channel->read();159if ($in_message !== null) {160$this->writeIORead($in_message);161}162}163164$out_message = $command_channel->read();165if (strlen($out_message)) {166$out_message = $this->willReadData($out_message);167if ($out_message !== null) {168$io_channel->write($out_message);169}170}171172// If we have nothing left on stdin, close stdin on the subprocess.173if (!$io_channel->isOpenForReading()) {174$command_channel->closeWriteChannel();175}176177if ($done) {178break;179}180181// If the client has disconnected, kill the subprocess and bail.182if (!$io_channel->isOpenForWriting()) {183$this->execFuture184->setStdoutSizeLimit(0)185->setStderrSizeLimit(0)186->setReadBufferSize(null)187->resolveKill();188break;189}190}191192list($err) = $this->execFuture193->setStdoutSizeLimit(0)194->setStderrSizeLimit(0)195->setReadBufferSize(null)196->resolve();197198return $err;199}200201public function writeIORead($in_message) {202$in_message = $this->willWriteData($in_message);203if (strlen($in_message)) {204$this->commandChannel->write($in_message);205}206}207208public function willWriteData($message) {209if ($this->willWriteCallback) {210return call_user_func($this->willWriteCallback, $this, $message);211} else {212if (strlen($message)) {213return $message;214} else {215return null;216}217}218}219220public function willReadData($message) {221if ($this->willReadCallback) {222return call_user_func($this->willReadCallback, $this, $message);223} else {224if (strlen($message)) {225return $message;226} else {227return null;228}229}230}231232}233234235