[ Index ]

PHP Cross Reference of Phabricator

title

Body

[close]

/src/applications/diffusion/ssh/ -> DiffusionSSHSubversionServeWorkflow.php (source)

   1  <?php
   2  
   3  /**
   4   * This protocol has a good spec here:
   5   *
   6   *   http://svn.apache.org/repos/asf/subversion/trunk/subversion/libsvn_ra_svn/protocol
   7   *
   8   */
   9  final class DiffusionSSHSubversionServeWorkflow
  10    extends DiffusionSSHSubversionWorkflow {
  11  
  12    private $didSeeWrite;
  13  
  14    private $inProtocol;
  15    private $outProtocol;
  16  
  17    private $inSeenGreeting;
  18  
  19    private $outPhaseCount = 0;
  20  
  21    private $internalBaseURI;
  22    private $externalBaseURI;
  23  
  24    public function didConstruct() {
  25      $this->setName('svnserve');
  26      $this->setArguments(
  27        array(
  28          array(
  29            'name' => 'tunnel',
  30            'short' => 't',
  31          ),
  32        ));
  33    }
  34  
  35    protected function executeRepositoryOperations() {
  36      $args = $this->getArgs();
  37      if (!$args->getArg('tunnel')) {
  38        throw new Exception('Expected `svnserve -t`!');
  39      }
  40  
  41      $command = csprintf(
  42        'svnserve -t --tunnel-user=%s',
  43        $this->getUser()->getUsername());
  44      $command = PhabricatorDaemon::sudoCommandAsDaemonUser($command);
  45  
  46      $future = new ExecFuture('%C', $command);
  47  
  48      $this->inProtocol = new DiffusionSubversionWireProtocol();
  49      $this->outProtocol = new DiffusionSubversionWireProtocol();
  50  
  51      $err = id($this->newPassthruCommand())
  52        ->setIOChannel($this->getIOChannel())
  53        ->setCommandChannelFromExecFuture($future)
  54        ->setWillWriteCallback(array($this, 'willWriteMessageCallback'))
  55        ->setWillReadCallback(array($this, 'willReadMessageCallback'))
  56        ->execute();
  57  
  58      if (!$err && $this->didSeeWrite) {
  59        $this->getRepository()->writeStatusMessage(
  60          PhabricatorRepositoryStatusMessage::TYPE_NEEDS_UPDATE,
  61          PhabricatorRepositoryStatusMessage::CODE_OKAY);
  62      }
  63  
  64      return $err;
  65    }
  66  
  67    public function willWriteMessageCallback(
  68      PhabricatorSSHPassthruCommand $command,
  69      $message) {
  70  
  71      $proto = $this->inProtocol;
  72      $messages = $proto->writeData($message);
  73  
  74      $result = array();
  75      foreach ($messages as $message) {
  76        $message_raw = $message['raw'];
  77        $struct = $message['structure'];
  78  
  79        if (!$this->inSeenGreeting) {
  80          $this->inSeenGreeting = true;
  81  
  82          // The first message the client sends looks like:
  83          //
  84          //   ( version ( cap1 ... ) url ... )
  85          //
  86          // We want to grab the URL, load the repository, make sure it exists and
  87          // is accessible, and then replace it with the location of the
  88          // repository on disk.
  89  
  90          $uri = $struct[2]['value'];
  91          $struct[2]['value'] = $this->makeInternalURI($uri);
  92  
  93          $message_raw = $proto->serializeStruct($struct);
  94        } else if (isset($struct[0]) && $struct[0]['type'] == 'word') {
  95  
  96          if (!$proto->isReadOnlyCommand($struct)) {
  97            $this->didSeeWrite = true;
  98            $this->requireWriteAccess($struct[0]['value']);
  99          }
 100  
 101          // Several other commands also pass in URLs. We need to translate
 102          // all of these into the internal representation; this also makes sure
 103          // they're valid and accessible.
 104  
 105          switch ($struct[0]['value']) {
 106            case 'reparent':
 107              // ( reparent ( url ) )
 108              $struct[1]['value'][0]['value'] = $this->makeInternalURI(
 109                $struct[1]['value'][0]['value']);
 110              $message_raw = $proto->serializeStruct($struct);
 111              break;
 112            case 'switch':
 113              // ( switch ( ( rev ) target recurse url ... ) )
 114              $struct[1]['value'][3]['value'] = $this->makeInternalURI(
 115                $struct[1]['value'][3]['value']);
 116              $message_raw = $proto->serializeStruct($struct);
 117              break;
 118            case 'diff':
 119              // ( diff ( ( rev ) target recurse ignore-ancestry url ... ) )
 120              $struct[1]['value'][4]['value'] = $this->makeInternalURI(
 121                $struct[1]['value'][4]['value']);
 122              $message_raw = $proto->serializeStruct($struct);
 123              break;
 124            case 'add-file':
 125              // ( add-file ( path dir-token file-token [ copy-path copy-rev ] ) )
 126              if (isset($struct[1]['value'][3]['value'][0]['value'])) {
 127                $copy_from = $struct[1]['value'][3]['value'][0]['value'];
 128                $copy_from = $this->makeInternalURI($copy_from);
 129                $struct[1]['value'][3]['value'][0]['value'] = $copy_from;
 130              }
 131              $message_raw = $proto->serializeStruct($struct);
 132              break;
 133          }
 134        }
 135  
 136        $result[] = $message_raw;
 137      }
 138  
 139      if (!$result) {
 140        return null;
 141      }
 142  
 143      return implode('', $result);
 144    }
 145  
 146    public function willReadMessageCallback(
 147      PhabricatorSSHPassthruCommand $command,
 148      $message) {
 149  
 150      $proto = $this->outProtocol;
 151      $messages = $proto->writeData($message);
 152  
 153      $result = array();
 154      foreach ($messages as $message) {
 155        $message_raw = $message['raw'];
 156        $struct = $message['structure'];
 157  
 158        if (isset($struct[0]) && ($struct[0]['type'] == 'word')) {
 159  
 160          if ($struct[0]['value'] == 'success') {
 161            switch ($this->outPhaseCount) {
 162              case 0:
 163                // This is the "greeting", which announces capabilities.
 164                break;
 165              case 1:
 166                // This responds to the client greeting, and announces auth.
 167                break;
 168              case 2:
 169                // This responds to auth, which should be trivial over SSH.
 170                break;
 171              case 3:
 172                // This contains the URI of the repository. We need to edit it;
 173                // if it does not match what the client requested it will reject
 174                // the response.
 175                $struct[1]['value'][1]['value'] = $this->makeExternalURI(
 176                  $struct[1]['value'][1]['value']);
 177                $message_raw = $proto->serializeStruct($struct);
 178                break;
 179              default:
 180                // We don't care about other protocol frames.
 181                break;
 182            }
 183  
 184            $this->outPhaseCount++;
 185          } else if ($struct[0]['value'] == 'failure') {
 186            // Find any error messages which include the internal URI, and
 187            // replace the text with the external URI.
 188            foreach ($struct[1]['value'] as $key => $error) {
 189              $code = $error['value'][0]['value'];
 190              $message = $error['value'][1]['value'];
 191  
 192              $message = str_replace(
 193                $this->internalBaseURI,
 194                $this->externalBaseURI,
 195                $message);
 196  
 197              // Derp derp derp derp derp. The structure looks like this:
 198              //   ( failure ( ( code message ... ) ... ) )
 199              $struct[1]['value'][$key]['value'][1]['value'] = $message;
 200            }
 201            $message_raw = $proto->serializeStruct($struct);
 202          }
 203  
 204        }
 205  
 206        $result[] = $message_raw;
 207      }
 208  
 209      if (!$result) {
 210        return null;
 211      }
 212  
 213      return implode('', $result);
 214    }
 215  
 216    private function makeInternalURI($uri_string) {
 217      $uri = new PhutilURI($uri_string);
 218  
 219      $proto = $uri->getProtocol();
 220      if ($proto !== 'svn+ssh') {
 221        throw new Exception(
 222          pht(
 223            'Protocol for URI "%s" MUST be "svn+ssh".',
 224            $uri_string));
 225      }
 226  
 227      $path = $uri->getPath();
 228  
 229      // Subversion presumably deals with this, but make sure there's nothing
 230      // skethcy going on with the URI.
 231      if (preg_match('(/\\.\\./)', $path)) {
 232        throw new Exception(
 233          pht(
 234            'String "/../" is invalid in path specification "%s".',
 235            $uri_string));
 236      }
 237  
 238      $repository = $this->loadRepository($path);
 239  
 240      $path = preg_replace(
 241        '(^/diffusion/[A-Z]+)',
 242        rtrim($repository->getLocalPath(), '/'),
 243        $path);
 244  
 245      if (preg_match('(^/diffusion/[A-Z]+/\z)', $path)) {
 246        $path = rtrim($path, '/');
 247      }
 248  
 249      $uri->setPath($path);
 250  
 251      // If this is happening during the handshake, these are the base URIs for
 252      // the request.
 253      if ($this->externalBaseURI === null) {
 254        $pre = (string)id(clone $uri)->setPath('');
 255        $this->externalBaseURI = $pre.'/diffusion/'.$repository->getCallsign();
 256        $this->internalBaseURI = $pre.rtrim($repository->getLocalPath(), '/');
 257      }
 258  
 259      return (string)$uri;
 260    }
 261  
 262    private function makeExternalURI($uri) {
 263      $internal = $this->internalBaseURI;
 264      $external = $this->externalBaseURI;
 265  
 266      if (strncmp($uri, $internal, strlen($internal)) === 0) {
 267        $uri = $external.substr($uri, strlen($internal));
 268      }
 269  
 270      return $uri;
 271    }
 272  
 273  }


Generated: Sun Nov 30 09:20:46 2014 Cross-referenced by PHPXref 0.7.1