1
0
Fork 0
mirror of https://we.phorge.it/source/phorge.git synced 2024-12-01 11:12:42 +01:00
phorge-phorge/src/applications/diffusion/ssh/DiffusionSubversionServeSSHWorkflow.php
Joshua Spence b6d745b666 Extend from Phobject
Summary: All classes should extend from some other class. See D13275 for some explanation.

Test Plan: `arc unit`

Reviewers: epriestley, #blessed_reviewers

Reviewed By: epriestley, #blessed_reviewers

Subscribers: epriestley, Korvin

Differential Revision: https://secure.phabricator.com/D13283
2015-06-15 18:02:27 +10:00

432 lines
13 KiB
PHP

<?php
/**
* This protocol has a good spec here:
*
* http://svn.apache.org/repos/asf/subversion/trunk/subversion/libsvn_ra_svn/protocol
*/
final class DiffusionSubversionServeSSHWorkflow
extends DiffusionSubversionSSHWorkflow {
private $didSeeWrite;
private $inProtocol;
private $outProtocol;
private $inSeenGreeting;
private $outPhaseCount = 0;
private $internalBaseURI;
private $externalBaseURI;
private $peekBuffer;
private $command;
private function getCommand() {
return $this->command;
}
protected function didConstruct() {
$this->setName('svnserve');
$this->setArguments(
array(
array(
'name' => 'tunnel',
'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(
'%s subprocess exited before emitting a protocol frame.',
'svnserve'));
}
}
$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() {
$repository = $this->getRepository();
$args = $this->getArgs();
if (!$args->getArg('tunnel')) {
throw new Exception(pht('Expected `%s`!', 'svnserve -t'));
}
if ($this->shouldProxy()) {
$command = $this->getProxyCommand();
} else {
$command = csprintf(
'svnserve -t --tunnel-user=%s',
$this->getUser()->getUsername());
}
$command = PhabricatorDaemon::sudoCommandAsDaemonUser($command);
$future = new ExecFuture('%C', $command);
$this->inProtocol = new DiffusionSubversionWireProtocol();
$this->outProtocol = new DiffusionSubversionWireProtocol();
$this->command = id($this->newPassthruCommand())
->setIOChannel($this->getIOChannel())
->setCommandChannelFromExecFuture($future)
->setWillWriteCallback(array($this, 'willWriteMessageCallback'))
->setWillReadCallback(array($this, 'willReadMessageCallback'));
$this->command->setPauseIOReads(true);
$err = $this->command->execute();
if (!$err && $this->didSeeWrite) {
$this->getRepository()->writeStatusMessage(
PhabricatorRepositoryStatusMessage::TYPE_NEEDS_UPDATE,
PhabricatorRepositoryStatusMessage::CODE_OKAY);
}
return $err;
}
public function willWriteMessageCallback(
PhabricatorSSHPassthruCommand $command,
$message) {
$proto = $this->inProtocol;
$messages = $proto->writeData($message);
$result = array();
foreach ($messages as $message) {
$message_raw = $message['raw'];
$struct = $message['structure'];
if (!$this->inSeenGreeting) {
$this->inSeenGreeting = true;
// The first message the client sends looks like:
//
// ( version ( cap1 ... ) url ... )
//
// We want to grab the URL, load the repository, make sure it exists and
// is accessible, and then replace it with the location of the
// repository on disk.
$uri = $struct[2]['value'];
$struct[2]['value'] = $this->makeInternalURI($uri);
$message_raw = $proto->serializeStruct($struct);
} else if (isset($struct[0]) && $struct[0]['type'] == 'word') {
if (!$proto->isReadOnlyCommand($struct)) {
$this->didSeeWrite = true;
$this->requireWriteAccess($struct[0]['value']);
}
// Several other commands also pass in URLs. We need to translate
// all of these into the internal representation; this also makes sure
// they're valid and accessible.
switch ($struct[0]['value']) {
case 'reparent':
// ( reparent ( url ) )
$struct[1]['value'][0]['value'] = $this->makeInternalURI(
$struct[1]['value'][0]['value']);
$message_raw = $proto->serializeStruct($struct);
break;
case 'switch':
// ( switch ( ( rev ) target recurse url ... ) )
$struct[1]['value'][3]['value'] = $this->makeInternalURI(
$struct[1]['value'][3]['value']);
$message_raw = $proto->serializeStruct($struct);
break;
case 'diff':
// ( diff ( ( rev ) target recurse ignore-ancestry url ... ) )
$struct[1]['value'][4]['value'] = $this->makeInternalURI(
$struct[1]['value'][4]['value']);
$message_raw = $proto->serializeStruct($struct);
break;
case 'add-file':
case 'add-dir':
// ( add-file ( path dir-token file-token [ copy-path copy-rev ] ) )
// ( add-dir ( path parent child [ copy-path copy-rev ] ) )
if (isset($struct[1]['value'][3]['value'][0]['value'])) {
$copy_from = $struct[1]['value'][3]['value'][0]['value'];
$copy_from = $this->makeInternalURI($copy_from);
$struct[1]['value'][3]['value'][0]['value'] = $copy_from;
}
$message_raw = $proto->serializeStruct($struct);
break;
}
}
$result[] = $message_raw;
}
if (!$result) {
return null;
}
return implode('', $result);
}
public function willReadMessageCallback(
PhabricatorSSHPassthruCommand $command,
$message) {
$proto = $this->outProtocol;
$messages = $proto->writeData($message);
$result = array();
foreach ($messages as $message) {
$message_raw = $message['raw'];
$struct = $message['structure'];
if (isset($struct[0]) && ($struct[0]['type'] == 'word')) {
if ($struct[0]['value'] == 'success') {
switch ($this->outPhaseCount) {
case 0:
// 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;
case 1:
// This responds to the client greeting, and announces auth.
break;
case 2:
// This responds to auth, which should be trivial over SSH.
break;
case 3:
// This contains the URI of the repository. We need to edit it;
// if it does not match what the client requested it will reject
// the response.
$struct[1]['value'][1]['value'] = $this->makeExternalURI(
$struct[1]['value'][1]['value']);
$message_raw = $proto->serializeStruct($struct);
break;
default:
// We don't care about other protocol frames.
break;
}
$this->outPhaseCount++;
} else if ($struct[0]['value'] == 'failure') {
// Find any error messages which include the internal URI, and
// replace the text with the external URI.
foreach ($struct[1]['value'] as $key => $error) {
$code = $error['value'][0]['value'];
$message = $error['value'][1]['value'];
$message = str_replace(
$this->internalBaseURI,
$this->externalBaseURI,
$message);
// Derp derp derp derp derp. The structure looks like this:
// ( failure ( ( code message ... ) ... ) )
$struct[1]['value'][$key]['value'][1]['value'] = $message;
}
$message_raw = $proto->serializeStruct($struct);
}
}
if ($message_raw !== null) {
$result[] = $message_raw;
}
}
if (!$result) {
return null;
}
return implode('', $result);
}
private function getPathFromSubversionURI($uri_string) {
$uri = new PhutilURI($uri_string);
$proto = $uri->getProtocol();
if ($proto !== 'svn+ssh') {
throw new Exception(
pht(
'Protocol for URI "%s" MUST be "%s".',
$uri_string,
'svn+ssh'));
}
$path = $uri->getPath();
// Subversion presumably deals with this, but make sure there's nothing
// sketchy going on with the URI.
if (preg_match('(/\\.\\./)', $path)) {
throw new Exception(
pht(
'String "%s" is invalid in path specification "%s".',
'/../',
$uri_string));
}
$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(
'(^/diffusion/[A-Z]+)',
rtrim($repository->getLocalPath(), '/'),
$path);
if (preg_match('(^/diffusion/[A-Z]+/\z)', $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);
// If this is happening during the handshake, these are the base URIs for
// the request.
if ($this->externalBaseURI === null) {
$pre = (string)id(clone $uri)->setPath('');
$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;
}
private function makeExternalURI($uri) {
$internal = $this->internalBaseURI;
$external = $this->externalBaseURI;
if (strncmp($uri, $internal, strlen($internal)) === 0) {
$uri = $external.substr($uri, strlen($internal));
}
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;
}
}