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.
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):
14 * IO Channel Command Channel
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.
22 * IO Channel Passthru Command Channel
23 * stdout -> willWrite -> stdin
24 * stdin <- willRead <- stdout
25 * stderr <- (identity) <- stderr
27 * Primarily, this means:
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.
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.
38 final class PhabricatorSSHPassthruCommand
extends Phobject
{
40 private $commandChannel;
42 private $errorChannel;
44 private $willWriteCallback;
45 private $willReadCallback;
46 private $pauseIOReads;
48 public function setCommandChannelFromExecFuture(ExecFuture
$exec_future) {
49 $exec_channel = new PhutilExecChannel($exec_future);
50 $exec_channel->setStderrHandler(array($this, 'writeErrorIOCallback'));
52 $this->execFuture
= $exec_future;
53 $this->commandChannel
= $exec_channel;
58 public function setIOChannel(PhutilChannel
$io_channel) {
59 $this->ioChannel
= $io_channel;
63 public function setErrorChannel(PhutilChannel
$error_channel) {
64 $this->errorChannel
= $error_channel;
68 public function setWillReadCallback($will_read_callback) {
69 $this->willReadCallback
= $will_read_callback;
73 public function setWillWriteCallback($will_write_callback) {
74 $this->willWriteCallback
= $will_write_callback;
78 public function writeErrorIOCallback(PhutilChannel
$channel, $data) {
79 $this->errorChannel
->write($data);
82 public function setPauseIOReads($pause) {
83 $this->pauseIOReads
= $pause;
87 public function execute() {
88 $command_channel = $this->commandChannel
;
89 $io_channel = $this->ioChannel
;
90 $error_channel = $this->errorChannel
;
92 if (!$command_channel) {
95 'Set a command channel before calling %s!',
102 'Set an IO channel before calling %s!',
106 if (!$error_channel) {
109 'Set an error channel before calling %s!',
113 $channels = array($command_channel, $io_channel, $error_channel);
115 // We want to limit the amount of data we'll hold in memory for this
116 // process. See T4241 for a discussion of this issue in general.
118 $buffer_size = (1024 * 1024); // 1MB
119 $io_channel->setReadBufferSize($buffer_size);
120 $command_channel->setReadBufferSize($buffer_size);
122 // TODO: This just makes us throw away stderr after the first 1MB, but we
123 // don't currently have the support infrastructure to buffer it correctly.
124 // It's difficult to imagine this causing problems in practice, though.
125 $this->execFuture
->getStderrSizeLimit($buffer_size);
128 PhutilChannel
::waitForAny($channels);
130 $io_channel->update();
131 $command_channel->update();
132 $error_channel->update();
134 // If any channel is blocked on the other end, wait for it to flush before
135 // we continue reading. For example, if a user is running `git clone` on
136 // a 1GB repository, the underlying `git-upload-pack` may
137 // be able to produce data much more quickly than we can send it over
138 // the network. If we don't throttle the reads, we may only send a few
139 // MB over the I/O channel in the time it takes to read the entire 1GB off
140 // the command channel. That leaves us with 1GB of data in memory.
142 while ($command_channel->isOpen() &&
143 $io_channel->isOpenForWriting() &&
144 ($command_channel->getWriteBufferSize() >= $buffer_size ||
145 $io_channel->getWriteBufferSize() >= $buffer_size ||
146 $error_channel->getWriteBufferSize() >= $buffer_size)) {
147 PhutilChannel
::waitForActivity(array(), $channels);
148 $io_channel->update();
149 $command_channel->update();
150 $error_channel->update();
153 // If the subprocess has exited and we've read everything from it,
155 $done = !$command_channel->isOpenForReading() &&
156 $command_channel->isReadBufferEmpty();
158 if (!$this->pauseIOReads
) {
159 $in_message = $io_channel->read();
160 if ($in_message !== null) {
161 $this->writeIORead($in_message);
165 $out_message = $command_channel->read();
166 if (strlen($out_message)) {
167 $out_message = $this->willReadData($out_message);
168 if ($out_message !== null) {
169 $io_channel->write($out_message);
173 // If we have nothing left on stdin, close stdin on the subprocess.
174 if (!$io_channel->isOpenForReading()) {
175 $command_channel->closeWriteChannel();
182 // If the client has disconnected, kill the subprocess and bail.
183 if (!$io_channel->isOpenForWriting()) {
185 ->setStdoutSizeLimit(0)
186 ->setStderrSizeLimit(0)
187 ->setReadBufferSize(null)
193 list($err) = $this->execFuture
194 ->setStdoutSizeLimit(0)
195 ->setStderrSizeLimit(0)
196 ->setReadBufferSize(null)
202 public function writeIORead($in_message) {
203 $in_message = $this->willWriteData($in_message);
204 if (strlen($in_message)) {
205 $this->commandChannel
->write($in_message);
209 public function willWriteData($message) {
210 if ($this->willWriteCallback
) {
211 return call_user_func($this->willWriteCallback
, $this, $message);
213 if (strlen($message)) {
221 public function willReadData($message) {
222 if ($this->willReadCallback
) {
223 return call_user_func($this->willReadCallback
, $this, $message);
225 if (strlen($message)) {