Differential D11541 Diff 27767 src/applications/diffusion/ssh/DiffusionSubversionServeSSHWorkflow.php
Changeset View
Changeset View
Standalone View
Standalone View
src/applications/diffusion/ssh/DiffusionSubversionServeSSHWorkflow.php
| Show All 14 Lines | final class DiffusionSubversionServeSSHWorkflow | ||||
| private $outProtocol; | private $outProtocol; | ||||
| private $inSeenGreeting; | private $inSeenGreeting; | ||||
| private $outPhaseCount = 0; | private $outPhaseCount = 0; | ||||
| private $internalBaseURI; | private $internalBaseURI; | ||||
| private $externalBaseURI; | private $externalBaseURI; | ||||
| private $peekBuffer; | |||||
| private $command; | |||||
| private function getCommand() { | |||||
| return $this->command; | |||||
| } | |||||
| protected function didConstruct() { | protected function didConstruct() { | ||||
| $this->setName('svnserve'); | $this->setName('svnserve'); | ||||
| $this->setArguments( | $this->setArguments( | ||||
| array( | array( | ||||
| array( | array( | ||||
| 'name' => 'tunnel', | 'name' => 'tunnel', | ||||
| 'short' => 't', | 'short' => 't', | ||||
| ), | ), | ||||
| )); | )); | ||||
| } | } | ||||
| protected function identifyRepository() { | |||||
| // NOTE: In SVN, we need to read the first few protocol frames before we | |||||
| // can determine which repository the user is trying to access. We're | |||||
| // going to peek at the data on the wire to identify the repository. | |||||
| $io_channel = $this->getIOChannel(); | |||||
| // Before the client will send us the first protocol frame, we need to send | |||||
| // it a connection frame with server capabilities. To figure out the | |||||
| // correct frame we're going to start `svnserve`, read the frame from it, | |||||
| // send it to the client, then kill the subprocess. | |||||
| // TODO: This is pretty inelegant and the protocol frame will change very | |||||
| // rarely. We could cache it if we can find a reasonable way to dirty the | |||||
| // cache. | |||||
| $command = csprintf('svnserve -t'); | |||||
| $command = PhabricatorDaemon::sudoCommandAsDaemonUser($command); | |||||
| $future = new ExecFuture('%C', $command); | |||||
| $exec_channel = new PhutilExecChannel($future); | |||||
| $exec_protocol = new DiffusionSubversionWireProtocol(); | |||||
| while (true) { | |||||
| PhutilChannel::waitForAny(array($exec_channel)); | |||||
| $exec_channel->update(); | |||||
| $exec_message = $exec_channel->read(); | |||||
| if ($exec_message !== null) { | |||||
| $messages = $exec_protocol->writeData($exec_message); | |||||
| if ($messages) { | |||||
| $message = head($messages); | |||||
| $raw = $message['raw']; | |||||
| // Write the greeting frame to the client. | |||||
| $io_channel->write($raw); | |||||
| // Kill the subprocess. | |||||
| $future->resolveKill(); | |||||
| break; | |||||
| } | |||||
| } | |||||
| if (!$exec_channel->isOpenForReading()) { | |||||
| throw new Exception( | |||||
| pht( | |||||
| 'svnserve subprocess exited before emitting a protocol frame.')); | |||||
| } | |||||
| } | |||||
| $io_protocol = new DiffusionSubversionWireProtocol(); | |||||
| while (true) { | |||||
| PhutilChannel::waitForAny(array($io_channel)); | |||||
| $io_channel->update(); | |||||
| $in_message = $io_channel->read(); | |||||
| if ($in_message !== null) { | |||||
| $this->peekBuffer .= $in_message; | |||||
| if (strlen($this->peekBuffer) > (1024 * 1024)) { | |||||
| throw new Exception( | |||||
| pht( | |||||
| 'Client transmitted more than 1MB of data without transmitting '. | |||||
| 'a recognizable protocol frame.')); | |||||
| } | |||||
| $messages = $io_protocol->writeData($in_message); | |||||
| if ($messages) { | |||||
| $message = head($messages); | |||||
| $struct = $message['structure']; | |||||
| // This is the: | |||||
| // | |||||
| // ( version ( cap1 ... ) url ... ) | |||||
| // | |||||
| // The `url` allows us to identify the repository. | |||||
| $uri = $struct[2]['value']; | |||||
| $path = $this->getPathFromSubversionURI($uri); | |||||
| return $this->loadRepositoryWithPath($path); | |||||
| } | |||||
| } | |||||
| if (!$io_channel->isOpenForReading()) { | |||||
| throw new Exception( | |||||
| pht( | |||||
| 'Client closed connection before sending a complete protocol '. | |||||
| 'frame.')); | |||||
| } | |||||
| // If the client has disconnected, kill the subprocess and bail. | |||||
| if (!$io_channel->isOpenForWriting()) { | |||||
| throw new Exception( | |||||
| pht( | |||||
| 'Client closed connection before receiving response.')); | |||||
| } | |||||
| } | |||||
| } | |||||
| protected function executeRepositoryOperations() { | protected function executeRepositoryOperations() { | ||||
| $repository = $this->getRepository(); | |||||
| $args = $this->getArgs(); | $args = $this->getArgs(); | ||||
| if (!$args->getArg('tunnel')) { | if (!$args->getArg('tunnel')) { | ||||
| throw new Exception('Expected `svnserve -t`!'); | throw new Exception('Expected `svnserve -t`!'); | ||||
| } | } | ||||
| $command = csprintf( | $command = csprintf( | ||||
| 'svnserve -t --tunnel-user=%s', | 'svnserve -t --tunnel-user=%s', | ||||
| $this->getUser()->getUsername()); | $this->getUser()->getUsername()); | ||||
| $command = PhabricatorDaemon::sudoCommandAsDaemonUser($command); | $command = PhabricatorDaemon::sudoCommandAsDaemonUser($command); | ||||
| $future = new ExecFuture('%C', $command); | $future = new ExecFuture('%C', $command); | ||||
| $this->inProtocol = new DiffusionSubversionWireProtocol(); | $this->inProtocol = new DiffusionSubversionWireProtocol(); | ||||
| $this->outProtocol = new DiffusionSubversionWireProtocol(); | $this->outProtocol = new DiffusionSubversionWireProtocol(); | ||||
| $err = id($this->newPassthruCommand()) | $this->command = id($this->newPassthruCommand()) | ||||
| ->setIOChannel($this->getIOChannel()) | ->setIOChannel($this->getIOChannel()) | ||||
| ->setCommandChannelFromExecFuture($future) | ->setCommandChannelFromExecFuture($future) | ||||
| ->setWillWriteCallback(array($this, 'willWriteMessageCallback')) | ->setWillWriteCallback(array($this, 'willWriteMessageCallback')) | ||||
| ->setWillReadCallback(array($this, 'willReadMessageCallback')) | ->setWillReadCallback(array($this, 'willReadMessageCallback')); | ||||
| ->execute(); | |||||
| $this->command->setPauseIOReads(true); | |||||
| $err = $this->command->execute(); | |||||
| if (!$err && $this->didSeeWrite) { | if (!$err && $this->didSeeWrite) { | ||||
| $this->getRepository()->writeStatusMessage( | $this->getRepository()->writeStatusMessage( | ||||
| PhabricatorRepositoryStatusMessage::TYPE_NEEDS_UPDATE, | PhabricatorRepositoryStatusMessage::TYPE_NEEDS_UPDATE, | ||||
| PhabricatorRepositoryStatusMessage::CODE_OKAY); | PhabricatorRepositoryStatusMessage::CODE_OKAY); | ||||
| } | } | ||||
| return $err; | return $err; | ||||
| ▲ Show 20 Lines • Show All 91 Lines • ▼ Show 20 Lines | foreach ($messages as $message) { | ||||
| $struct = $message['structure']; | $struct = $message['structure']; | ||||
| if (isset($struct[0]) && ($struct[0]['type'] == 'word')) { | if (isset($struct[0]) && ($struct[0]['type'] == 'word')) { | ||||
| if ($struct[0]['value'] == 'success') { | if ($struct[0]['value'] == 'success') { | ||||
| switch ($this->outPhaseCount) { | switch ($this->outPhaseCount) { | ||||
| case 0: | case 0: | ||||
| // This is the "greeting", which announces capabilities. | // This is the "greeting", which announces capabilities. | ||||
| // We already sent this when we were figuring out which | |||||
| // repository this request is for, so we aren't going to send | |||||
| // it again. | |||||
| // Instead, we're going to replay the client's response (which | |||||
| // we also already read). | |||||
| $command = $this->getCommand(); | |||||
| $command->writeIORead($this->peekBuffer); | |||||
| $command->setPauseIOReads(false); | |||||
| $message_raw = null; | |||||
| break; | break; | ||||
| case 1: | case 1: | ||||
| // This responds to the client greeting, and announces auth. | // This responds to the client greeting, and announces auth. | ||||
| break; | break; | ||||
| case 2: | case 2: | ||||
| // This responds to auth, which should be trivial over SSH. | // This responds to auth, which should be trivial over SSH. | ||||
| break; | break; | ||||
| case 3: | case 3: | ||||
| Show All 26 Lines | foreach ($messages as $message) { | ||||
| // ( failure ( ( code message ... ) ... ) ) | // ( failure ( ( code message ... ) ... ) ) | ||||
| $struct[1]['value'][$key]['value'][1]['value'] = $message; | $struct[1]['value'][$key]['value'][1]['value'] = $message; | ||||
| } | } | ||||
| $message_raw = $proto->serializeStruct($struct); | $message_raw = $proto->serializeStruct($struct); | ||||
| } | } | ||||
| } | } | ||||
| if ($message_raw !== null) { | |||||
| $result[] = $message_raw; | $result[] = $message_raw; | ||||
| } | } | ||||
| } | |||||
| if (!$result) { | if (!$result) { | ||||
| return null; | return null; | ||||
| } | } | ||||
| return implode('', $result); | return implode('', $result); | ||||
| } | } | ||||
| private function makeInternalURI($uri_string) { | private function getPathFromSubversionURI($uri_string) { | ||||
| $uri = new PhutilURI($uri_string); | $uri = new PhutilURI($uri_string); | ||||
| $proto = $uri->getProtocol(); | $proto = $uri->getProtocol(); | ||||
| if ($proto !== 'svn+ssh') { | if ($proto !== 'svn+ssh') { | ||||
| throw new Exception( | throw new Exception( | ||||
| pht( | pht( | ||||
| 'Protocol for URI "%s" MUST be "svn+ssh".', | 'Protocol for URI "%s" MUST be "svn+ssh".', | ||||
| $uri_string)); | $uri_string)); | ||||
| } | } | ||||
| $path = $uri->getPath(); | $path = $uri->getPath(); | ||||
| // Subversion presumably deals with this, but make sure there's nothing | // Subversion presumably deals with this, but make sure there's nothing | ||||
| // skethcy going on with the URI. | // sketchy going on with the URI. | ||||
| if (preg_match('(/\\.\\./)', $path)) { | if (preg_match('(/\\.\\./)', $path)) { | ||||
| throw new Exception( | throw new Exception( | ||||
| pht( | pht( | ||||
| 'String "/../" is invalid in path specification "%s".', | 'String "/../" is invalid in path specification "%s".', | ||||
| $uri_string)); | $uri_string)); | ||||
| } | } | ||||
| $repository = $this->loadRepository($path); | $path = $this->normalizeSVNPath($path); | ||||
| return $path; | |||||
| } | |||||
| private function makeInternalURI($uri_string) { | |||||
| $uri = new PhutilURI($uri_string); | |||||
| $repository = $this->getRepository(); | |||||
| $path = $this->getPathFromSubversionURI($uri_string); | |||||
| $path = preg_replace( | $path = preg_replace( | ||||
| '(^/diffusion/[A-Z]+)', | '(^/diffusion/[A-Z]+)', | ||||
| rtrim($repository->getLocalPath(), '/'), | rtrim($repository->getLocalPath(), '/'), | ||||
| $path); | $path); | ||||
| if (preg_match('(^/diffusion/[A-Z]+/\z)', $path)) { | if (preg_match('(^/diffusion/[A-Z]+/\z)', $path)) { | ||||
| $path = rtrim($path, '/'); | $path = rtrim($path, '/'); | ||||
| } | } | ||||
| // NOTE: We are intentionally NOT removing username information from the | |||||
| // URI. Subversion retains it over the course of the request and considers | |||||
| // two repositories with different username identifiers to be distinct and | |||||
| // incompatible. | |||||
| $uri->setPath($path); | $uri->setPath($path); | ||||
| // If this is happening during the handshake, these are the base URIs for | // If this is happening during the handshake, these are the base URIs for | ||||
| // the request. | // the request. | ||||
| if ($this->externalBaseURI === null) { | if ($this->externalBaseURI === null) { | ||||
| $pre = (string)id(clone $uri)->setPath(''); | $pre = (string)id(clone $uri)->setPath(''); | ||||
| $this->externalBaseURI = $pre.'/diffusion/'.$repository->getCallsign(); | |||||
| $this->internalBaseURI = $pre.rtrim($repository->getLocalPath(), '/'); | $external_path = '/diffusion/'.$repository->getCallsign(); | ||||
| $external_path = $this->normalizeSVNPath($external_path); | |||||
| $this->externalBaseURI = $pre.$external_path; | |||||
| $internal_path = rtrim($repository->getLocalPath(), '/'); | |||||
| $internal_path = $this->normalizeSVNPath($internal_path); | |||||
| $this->internalBaseURI = $pre.$internal_path; | |||||
| } | } | ||||
| return (string)$uri; | return (string)$uri; | ||||
| } | } | ||||
| private function makeExternalURI($uri) { | private function makeExternalURI($uri) { | ||||
| $internal = $this->internalBaseURI; | $internal = $this->internalBaseURI; | ||||
| $external = $this->externalBaseURI; | $external = $this->externalBaseURI; | ||||
| if (strncmp($uri, $internal, strlen($internal)) === 0) { | if (strncmp($uri, $internal, strlen($internal)) === 0) { | ||||
| $uri = $external.substr($uri, strlen($internal)); | $uri = $external.substr($uri, strlen($internal)); | ||||
| } | } | ||||
| return $uri; | return $uri; | ||||
| } | } | ||||
| private function normalizeSVNPath($path) { | |||||
| // Subversion normalizes redundant slashes internally, so normalize them | |||||
| // here as well to make sure things match up. | |||||
| $path = preg_replace('(/+)', '/', $path); | |||||
| return $path; | |||||
| } | |||||
| } | } | ||||