diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php --- a/src/__phutil_library_map__.php +++ b/src/__phutil_library_map__.php @@ -662,6 +662,7 @@ 'DiffusionGitResponse' => 'applications/diffusion/response/DiffusionGitResponse.php', 'DiffusionGitSSHWorkflow' => 'applications/diffusion/ssh/DiffusionGitSSHWorkflow.php', 'DiffusionGitUploadPackSSHWorkflow' => 'applications/diffusion/ssh/DiffusionGitUploadPackSSHWorkflow.php', + 'DiffusionGitWireProtocol' => 'applications/diffusion/protocol/DiffusionGitWireProtocol.php', 'DiffusionHistoryController' => 'applications/diffusion/controller/DiffusionHistoryController.php', 'DiffusionHistoryQueryConduitAPIMethod' => 'applications/diffusion/conduit/DiffusionHistoryQueryConduitAPIMethod.php', 'DiffusionHistoryTableView' => 'applications/diffusion/view/DiffusionHistoryTableView.php', @@ -4906,6 +4907,7 @@ 'DiffusionRepositoryClusterEngineLogInterface', ), 'DiffusionGitUploadPackSSHWorkflow' => 'DiffusionGitSSHWorkflow', + 'DiffusionGitWireProtocol' => 'Phobject', 'DiffusionHistoryController' => 'DiffusionController', 'DiffusionHistoryQueryConduitAPIMethod' => 'DiffusionQueryConduitAPIMethod', 'DiffusionHistoryTableView' => 'DiffusionView', diff --git a/src/applications/diffusion/protocol/DiffusionGitWireProtocol.php b/src/applications/diffusion/protocol/DiffusionGitWireProtocol.php new file mode 100644 --- /dev/null +++ b/src/applications/diffusion/protocol/DiffusionGitWireProtocol.php @@ -0,0 +1,176 @@ +buffer .= $data; + + while (true) { + if ($this->state == 'head') { + if (strlen($this->buffer) < 4) { + break; + } + + $head = substr($this->buffer, 0, 4); + $this->buffer = substr($this->buffer, 4); + + if ($head == 'PACK') { + $this->state = 'pack-version'; + continue; + } + + if (!preg_match('/^[a-f0-9]{4}\z/', $head)) { + throw new Exception( + pht( + 'Expected Git wire protocol frame length header (four hex '. + 'characters, like "02af") but got "%s".', + $head)); + } + + $bytes = hexdec($head); + + // NOTE: This header is documented as containing the length of the + // "rest of the line", but in practice it appears to include its own + // length. + if ($bytes >= 4) { + $this->expectBytes = ($bytes - 4); + } else if (!$bytes) { + $this->expectBytes = $bytes; + } else { + throw new Exception( + pht( + 'Expected Git wire protocol frame length header with valid '. + 'value (0, or >= 4 bytes) but got invalid value ("%s").', + $bytes)); + } + + $this->state = 'body'; + } else if ($this->state == 'body') { + if (strlen($this->buffer) < $this->expectBytes) { + break; + } + + $body = substr($this->buffer, 0, $this->expectBytes); + $this->buffer = substr($this->buffer, $this->expectBytes); + + $this->state = 'head'; + } else if ($this->state == 'pack-version') { + if (strlen($this->buffer) < 4) { + break; + } + + $version = substr($this->buffer, 0, 4); + $this->buffer = substr($this->buffer, 4); + + $expect = "\x00\x00\x00\x02"; + if ($version !== $expect) { + throw new Exception( + pht( + 'Expected Git wire protocol packfile version 2 (four binary '. + 'bytes, "%s") but got "%s".', + bin2hex($expect), + bin2hex($version))); + } + + $this->state = 'pack-length'; + } else if ($this->state == 'pack-length') { + if (strlen($this->buffer) < 4) { + break; + } + + $length = substr($this->buffer, 0, 4); + $this->buffer = substr($this->buffer, 4); + + $value = head(unpack('N', $length)); + if (!$value) { + throw new Exception( + pht( + 'Expected Git wire protocol pack count (four binary '. + 'bytes) but got "%s".', + bin2hex($length))); + } + + $this->expectPacks = $value; + $this->state = 'pack-head'; + } else if ($this->state == 'pack-head') { + if (!$this->expectPacks) { + $this->state = 'pack-tail'; + continue; + } + + if (!strlen($this->buffer)) { + break; + } + + $this->expectPacks--; + + $byte = substr($this->buffer, 0, 1); + $this->buffer = substr($this->buffer, 1); + + $byte = ord($byte); + $this->packType = ($byte >> 4) & 7; + $this->packSize = ($byte & 15); + $this->packShift = 4; + + if ($byte & 0x80) { + $this->state = 'pack-head-more'; + } else { + $this->state = 'pack-body'; + } + } else if ($this->state == 'pack-head-more') { + if (!strlen($this->buffer)) { + break; + } + + $byte = substr($this->buffer, 0, 1); + $this->buffer = substr($this->buffer, 1); + + $byte = ord($byte); + + $this->packSize += ($byte & 0x7F) << $this->packShift; + $this->packShift += 7; + + if ($byte & 0x80) { + $this->state = 'pack-head-more'; + } else { + $this->state = 'pack-body'; + } + } else if ($this->state == 'pack-body') { + $available = strlen($this->buffer); + $expect = $this->packSize; + + $read_bytes = min($available, $expect); + if ($read_bytes) { + $pack_data = substr($this->buffer, 0, $read_bytes); + $this->buffer = substr($this->buffer, $read_bytes); + $this->packSize -= $read_bytes; + } + + if (!$this->expectBytes) { + $this->state = 'pack-head'; + } + } else if ($this->state == 'pack-tail') { + if (strlen($this->buffer) < 20) { + break; + } + + $sha1 = substr($this->buffer, 0, 20); + $this->buffer = substr($this->buffer, 20); + + $this->state = 'head'; + } + } + + return $data; + } + +} diff --git a/src/applications/diffusion/ssh/DiffusionGitReceivePackSSHWorkflow.php b/src/applications/diffusion/ssh/DiffusionGitReceivePackSSHWorkflow.php --- a/src/applications/diffusion/ssh/DiffusionGitReceivePackSSHWorkflow.php +++ b/src/applications/diffusion/ssh/DiffusionGitReceivePackSSHWorkflow.php @@ -2,6 +2,9 @@ final class DiffusionGitReceivePackSSHWorkflow extends DiffusionGitSSHWorkflow { + private $inProtocol; + private $outProtocol; + protected function didConstruct() { $this->setName('git-receive-pack'); $this->setArguments( @@ -28,6 +31,7 @@ if ($this->shouldProxy()) { $command = $this->getProxyCommand(); + $intercept = false; $did_synchronize = false; if ($device) { @@ -38,6 +42,7 @@ } } else { $command = csprintf('git-receive-pack %s', $repository->getLocalPath()); + $intercept = true; $did_synchronize = true; $cluster_engine->synchronizeWorkingCopyBeforeWrite(); @@ -51,7 +56,7 @@ $caught = null; try { - $err = $this->executeRepositoryCommand($command); + $err = $this->executeRepositoryCommand($command, $intercept); } catch (Exception $ex) { $caught = $ex; } @@ -76,17 +81,51 @@ return $err; } - private function executeRepositoryCommand($command) { + private function executeRepositoryCommand($command, $intercept) { $repository = $this->getRepository(); $command = PhabricatorDaemon::sudoCommandAsDaemonUser($command); $future = id(new ExecFuture('%C', $command)) ->setEnv($this->getEnvironment()); - return $this->newPassthruCommand() + $command = $this->newPassthruCommand() ->setIOChannel($this->getIOChannel()) - ->setCommandChannelFromExecFuture($future) - ->execute(); + ->setCommandChannelFromExecFuture($future); + + if ($intercept) { + $this->inProtocol = new DiffusionGitWireProtocol(); + $this->outProtocol = new DiffusionGitWireProtocol(); + + $command + ->setWillReadCallback(array($this, 'willReadMessageCallback')) + ->setWillWriteCallback(array($this, 'willWriteMessageCallback')); + } + + return $command->execute(); + } + + public function willReadMessageCallback( + PhabricatorSSHPassthruCommand $command, + $message) { + + $f = fopen('/tmp/git-response.log', 'a'); + fwrite($f, $message); + fflush($f); + fclose($f); + + return $this->outProtocol->writeData($message); + } + + public function willWriteMessageCallback( + PhabricatorSSHPassthruCommand $command, + $message) { + + $f = fopen('/tmp/git-request.log', 'a'); + fwrite($f, $message); + fflush($f); + fclose($f); + + return $this->inProtocol->writeData($message); } }