Allow Phabricator to serve Mercurial repositories over HTTP
Summary: Ref T2230. This is easily the worst thing I've had to write in a while. I'll leave some notes inline. Test Plan: Ran `hg clone http://...` on a hosted repo. Ran `hg push` on the same. Changed sync'd both ways. Reviewers: asherkin, btrahan Reviewed By: btrahan CC: aran Maniphest Tasks: T2230 Differential Revision: https://secure.phabricator.com/D7520
This commit is contained in:
		@@ -505,6 +505,8 @@ phutil_register_library_map(array(
 | 
				
			|||||||
    'DiffusionMercurialFileContentQuery' => 'applications/diffusion/query/filecontent/DiffusionMercurialFileContentQuery.php',
 | 
					    'DiffusionMercurialFileContentQuery' => 'applications/diffusion/query/filecontent/DiffusionMercurialFileContentQuery.php',
 | 
				
			||||||
    'DiffusionMercurialRawDiffQuery' => 'applications/diffusion/query/rawdiff/DiffusionMercurialRawDiffQuery.php',
 | 
					    'DiffusionMercurialRawDiffQuery' => 'applications/diffusion/query/rawdiff/DiffusionMercurialRawDiffQuery.php',
 | 
				
			||||||
    'DiffusionMercurialRequest' => 'applications/diffusion/request/DiffusionMercurialRequest.php',
 | 
					    'DiffusionMercurialRequest' => 'applications/diffusion/request/DiffusionMercurialRequest.php',
 | 
				
			||||||
 | 
					    'DiffusionMercurialResponse' => 'applications/diffusion/response/DiffusionMercurialResponse.php',
 | 
				
			||||||
 | 
					    'DiffusionMercurialWireProtocol' => 'applications/diffusion/protocol/DiffusionMercurialWireProtocol.php',
 | 
				
			||||||
    'DiffusionPathChange' => 'applications/diffusion/data/DiffusionPathChange.php',
 | 
					    'DiffusionPathChange' => 'applications/diffusion/data/DiffusionPathChange.php',
 | 
				
			||||||
    'DiffusionPathChangeQuery' => 'applications/diffusion/query/pathchange/DiffusionPathChangeQuery.php',
 | 
					    'DiffusionPathChangeQuery' => 'applications/diffusion/query/pathchange/DiffusionPathChangeQuery.php',
 | 
				
			||||||
    'DiffusionPathCompleteController' => 'applications/diffusion/controller/DiffusionPathCompleteController.php',
 | 
					    'DiffusionPathCompleteController' => 'applications/diffusion/controller/DiffusionPathCompleteController.php',
 | 
				
			||||||
@@ -2757,6 +2759,7 @@ phutil_register_library_map(array(
 | 
				
			|||||||
    'DiffusionMercurialFileContentQuery' => 'DiffusionFileContentQuery',
 | 
					    'DiffusionMercurialFileContentQuery' => 'DiffusionFileContentQuery',
 | 
				
			||||||
    'DiffusionMercurialRawDiffQuery' => 'DiffusionRawDiffQuery',
 | 
					    'DiffusionMercurialRawDiffQuery' => 'DiffusionRawDiffQuery',
 | 
				
			||||||
    'DiffusionMercurialRequest' => 'DiffusionRequest',
 | 
					    'DiffusionMercurialRequest' => 'DiffusionRequest',
 | 
				
			||||||
 | 
					    'DiffusionMercurialResponse' => 'AphrontResponse',
 | 
				
			||||||
    'DiffusionPathCompleteController' => 'DiffusionController',
 | 
					    'DiffusionPathCompleteController' => 'DiffusionController',
 | 
				
			||||||
    'DiffusionPathQueryTestCase' => 'PhabricatorTestCase',
 | 
					    'DiffusionPathQueryTestCase' => 'PhabricatorTestCase',
 | 
				
			||||||
    'DiffusionPathValidateController' => 'DiffusionController',
 | 
					    'DiffusionPathValidateController' => 'DiffusionController',
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -176,6 +176,9 @@ final class DiffusionServeController extends DiffusionController {
 | 
				
			|||||||
      case PhabricatorRepositoryType::REPOSITORY_TYPE_GIT:
 | 
					      case PhabricatorRepositoryType::REPOSITORY_TYPE_GIT:
 | 
				
			||||||
        $result = $this->serveGitRequest($repository, $viewer);
 | 
					        $result = $this->serveGitRequest($repository, $viewer);
 | 
				
			||||||
        break;
 | 
					        break;
 | 
				
			||||||
 | 
					      case PhabricatorRepositoryType::REPOSITORY_TYPE_MERCURIAL:
 | 
				
			||||||
 | 
					        $result = $this->serveMercurialRequest($repository, $viewer);
 | 
				
			||||||
 | 
					        break;
 | 
				
			||||||
      default:
 | 
					      default:
 | 
				
			||||||
        $result = new PhabricatorVCSResponse(
 | 
					        $result = new PhabricatorVCSResponse(
 | 
				
			||||||
          999,
 | 
					          999,
 | 
				
			||||||
@@ -224,13 +227,43 @@ final class DiffusionServeController extends DiffusionController {
 | 
				
			|||||||
        break;
 | 
					        break;
 | 
				
			||||||
      case PhabricatorRepositoryType::REPOSITORY_TYPE_MERCURIAL:
 | 
					      case PhabricatorRepositoryType::REPOSITORY_TYPE_MERCURIAL:
 | 
				
			||||||
        $cmd = $request->getStr('cmd');
 | 
					        $cmd = $request->getStr('cmd');
 | 
				
			||||||
        switch ($cmd) {
 | 
					        if ($cmd == 'batch') {
 | 
				
			||||||
          case 'capabilities':
 | 
					          // For "batch" we get a "cmds" argument like
 | 
				
			||||||
 | 
					          //
 | 
				
			||||||
 | 
					          //   heads ;known nodes=
 | 
				
			||||||
 | 
					          //
 | 
				
			||||||
 | 
					          // We need to examine the commands (here, "heads" and "known") to
 | 
				
			||||||
 | 
					          // make sure they're all read-only.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					          $args = $this->getMercurialArguments();
 | 
				
			||||||
 | 
					          $cmds = idx($args, 'cmds');
 | 
				
			||||||
 | 
					          if ($cmds) {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            // NOTE: Mercurial has some code to escape semicolons, but it does
 | 
				
			||||||
 | 
					            // not actually function for command separation. For example, these
 | 
				
			||||||
 | 
					            // two batch commands will produce completely different results (the
 | 
				
			||||||
 | 
					            // former will run the lookup; the latter will fail with a parser
 | 
				
			||||||
 | 
					            // error):
 | 
				
			||||||
 | 
					            //
 | 
				
			||||||
 | 
					            //  lookup key=a:xb;lookup key=z* 0
 | 
				
			||||||
 | 
					            //  lookup key=a:;b;lookup key=z* 0
 | 
				
			||||||
 | 
					            //               ^
 | 
				
			||||||
 | 
					            //               |
 | 
				
			||||||
 | 
					            //               +-- Note semicolon.
 | 
				
			||||||
 | 
					            //
 | 
				
			||||||
 | 
					            // So just split unconditionally.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            $cmds = explode(';', $cmds);
 | 
				
			||||||
 | 
					            foreach ($cmds as $sub_cmd) {
 | 
				
			||||||
 | 
					              $name = head(explode(' ', $sub_cmd, 2));
 | 
				
			||||||
 | 
					              if (!DiffusionMercurialWireProtocol::isReadOnlyCommand($name)) {
 | 
				
			||||||
 | 
					                return false;
 | 
				
			||||||
 | 
					              }
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
            return true;
 | 
					            return true;
 | 
				
			||||||
          default:
 | 
					          }
 | 
				
			||||||
            return false;
 | 
					 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
        break;
 | 
					        return DiffusionMercurialWireProtocol::isReadOnlyCommand($cmd);
 | 
				
			||||||
      case PhabricatorRepositoryType::REPOSITORY_TYPE_SUBVERSION:
 | 
					      case PhabricatorRepositoryType::REPOSITORY_TYPE_SUBVERSION:
 | 
				
			||||||
        break;
 | 
					        break;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
@@ -357,5 +390,127 @@ final class DiffusionServeController extends DiffusionController {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    return $user;
 | 
					    return $user;
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  private function serveMercurialRequest(PhabricatorRepository $repository) {
 | 
				
			||||||
 | 
					    $request = $this->getRequest();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    $bin = Filesystem::resolveBinary('hg');
 | 
				
			||||||
 | 
					    if (!$bin) {
 | 
				
			||||||
 | 
					      throw new Exception("Unable to find `hg` in PATH!");
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    $env = array();
 | 
				
			||||||
 | 
					    $input = PhabricatorStartup::getRawInput();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    $cmd = $request->getStr('cmd');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    $args = $this->getMercurialArguments();
 | 
				
			||||||
 | 
					    $args = $this->formatMercurialArguments($cmd, $args);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (strlen($input)) {
 | 
				
			||||||
 | 
					      $input = strlen($input)."\n".$input."0\n";
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    list($err, $stdout, $stderr) = id(new ExecFuture('%s serve --stdio', $bin))
 | 
				
			||||||
 | 
					      ->setEnv($env, true)
 | 
				
			||||||
 | 
					      ->setCWD($repository->getLocalPath())
 | 
				
			||||||
 | 
					      ->write("{$cmd}\n{$args}{$input}")
 | 
				
			||||||
 | 
					      ->resolve();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if ($err) {
 | 
				
			||||||
 | 
					      return new PhabricatorVCSResponse(
 | 
				
			||||||
 | 
					        500,
 | 
				
			||||||
 | 
					        pht('Error %d: %s', $err, $stderr));
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if ($cmd == 'getbundle' ||
 | 
				
			||||||
 | 
					        $cmd == 'changegroup' ||
 | 
				
			||||||
 | 
					        $cmd == 'changegroupsubset') {
 | 
				
			||||||
 | 
					      // We're not completely sure that "changegroup" and "changegroupsubset"
 | 
				
			||||||
 | 
					      // actually work, they're for very old Mercurial.
 | 
				
			||||||
 | 
					      $body = gzcompress($stdout);
 | 
				
			||||||
 | 
					    } else if ($cmd == 'unbundle') {
 | 
				
			||||||
 | 
					      // This includes diagnostic information and anything echoed by commit
 | 
				
			||||||
 | 
					      // hooks. We ignore `stdout` since it just has protocol garbage, and
 | 
				
			||||||
 | 
					      // substitute `stderr`.
 | 
				
			||||||
 | 
					      $body = strlen($stderr)."\n".$stderr;
 | 
				
			||||||
 | 
					    } else {
 | 
				
			||||||
 | 
					      list($length, $body) = explode("\n", $stdout, 2);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return id(new DiffusionMercurialResponse())->setContent($body);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  private function getMercurialArguments() {
 | 
				
			||||||
 | 
					    // Mercurial sends arguments in HTTP headers. "Why?", you might wonder,
 | 
				
			||||||
 | 
					    // "Why would you do this?".
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    $args_raw = array();
 | 
				
			||||||
 | 
					    for ($ii = 1; ; $ii++) {
 | 
				
			||||||
 | 
					      $header = 'HTTP_X_HGARG_'.$ii;
 | 
				
			||||||
 | 
					      if (!array_key_exists($header, $_SERVER)) {
 | 
				
			||||||
 | 
					        break;
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					      $args_raw[] = $_SERVER[$header];
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    $args_raw = implode('', $args_raw);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return id(new PhutilQueryStringParser())
 | 
				
			||||||
 | 
					      ->parseQueryString($args_raw);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  private function formatMercurialArguments($command, array $arguments) {
 | 
				
			||||||
 | 
					    $spec = DiffusionMercurialWireProtocol::getCommandArgs($command);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    $out = array();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // Mercurial takes normal arguments like this:
 | 
				
			||||||
 | 
					    //
 | 
				
			||||||
 | 
					    //   name <length(value)>
 | 
				
			||||||
 | 
					    //   value
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    $has_star = false;
 | 
				
			||||||
 | 
					    foreach ($spec as $arg_key) {
 | 
				
			||||||
 | 
					      if ($arg_key == '*') {
 | 
				
			||||||
 | 
					        $has_star = true;
 | 
				
			||||||
 | 
					        continue;
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					      if (isset($arguments[$arg_key])) {
 | 
				
			||||||
 | 
					        $value = $arguments[$arg_key];
 | 
				
			||||||
 | 
					        $size = strlen($value);
 | 
				
			||||||
 | 
					        $out[] = "{$arg_key} {$size}\n{$value}";
 | 
				
			||||||
 | 
					        unset($arguments[$arg_key]);
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if ($has_star) {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      // Mercurial takes arguments for variable argument lists roughly like
 | 
				
			||||||
 | 
					      // this:
 | 
				
			||||||
 | 
					      //
 | 
				
			||||||
 | 
					      //   * <count(args)>
 | 
				
			||||||
 | 
					      //   argname1 <length(argvalue1)>
 | 
				
			||||||
 | 
					      //   argvalue1
 | 
				
			||||||
 | 
					      //   argname2 <length(argvalue2)>
 | 
				
			||||||
 | 
					      //   argvalue2
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      $count = count($arguments);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      $out[] = "* {$count}\n";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      foreach ($arguments as $key => $value) {
 | 
				
			||||||
 | 
					        if (in_array($key, $spec)) {
 | 
				
			||||||
 | 
					          // We already added this argument above, so skip it.
 | 
				
			||||||
 | 
					          continue;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        $size = strlen($value);
 | 
				
			||||||
 | 
					        $out[] = "{$key} {$size}\n{$value}";
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return implode('', $out);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -0,0 +1,62 @@
 | 
				
			|||||||
 | 
					<?php
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					final class DiffusionMercurialWireProtocol {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  public static function getCommandArgs($command) {
 | 
				
			||||||
 | 
					    // We need to enumerate all of the Mercurial wire commands because the
 | 
				
			||||||
 | 
					    // argument encoding varies based on the command. "Why?", you might ask,
 | 
				
			||||||
 | 
					    // "Why would you do this?".
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    $commands = array(
 | 
				
			||||||
 | 
					      'batch' => array('cmds', '*'),
 | 
				
			||||||
 | 
					      'between' => array('pairs'),
 | 
				
			||||||
 | 
					      'branchmap' => array(),
 | 
				
			||||||
 | 
					      'branches' => array('nodes'),
 | 
				
			||||||
 | 
					      'capabilities' => array(),
 | 
				
			||||||
 | 
					      'changegroup' => array('roots'),
 | 
				
			||||||
 | 
					      'changegroupsubset' => array('bases heads'),
 | 
				
			||||||
 | 
					      'debugwireargs' => array('one two *'),
 | 
				
			||||||
 | 
					      'getbundle' => array('*'),
 | 
				
			||||||
 | 
					      'heads' => array(),
 | 
				
			||||||
 | 
					      'hello' => array(),
 | 
				
			||||||
 | 
					      'known' => array('nodes', '*'),
 | 
				
			||||||
 | 
					      'listkeys' => array('namespace'),
 | 
				
			||||||
 | 
					      'lookup' => array('key'),
 | 
				
			||||||
 | 
					      'pushkey' => array('namespace', 'key', 'old', 'new'),
 | 
				
			||||||
 | 
					      'stream_out' => array(''),
 | 
				
			||||||
 | 
					      'unbundle' => array('heads'),
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (!isset($commands[$command])) {
 | 
				
			||||||
 | 
					      throw new Exception("Unknown Mercurial command '{$command}!");
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return $commands[$command];
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  public static function isReadOnlyCommand($command) {
 | 
				
			||||||
 | 
					    $read_only = array(
 | 
				
			||||||
 | 
					      'between' => true,
 | 
				
			||||||
 | 
					      'branchmap' => true,
 | 
				
			||||||
 | 
					      'branches' => true,
 | 
				
			||||||
 | 
					      'capabilities' => true,
 | 
				
			||||||
 | 
					      'changegroup' => true,
 | 
				
			||||||
 | 
					      'changegroupsubset' => true,
 | 
				
			||||||
 | 
					      'debugwireargs' => true,
 | 
				
			||||||
 | 
					      'getbundle' => true,
 | 
				
			||||||
 | 
					      'heads' => true,
 | 
				
			||||||
 | 
					      'hello' => true,
 | 
				
			||||||
 | 
					      'known' => true,
 | 
				
			||||||
 | 
					      'listkeys' => true,
 | 
				
			||||||
 | 
					      'lookup' => true,
 | 
				
			||||||
 | 
					      'stream_out' => true,
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // Notably, the write commands are "pushkey" and "unbundle". The
 | 
				
			||||||
 | 
					    // "batch" command is theoretically read only, but we require explicit
 | 
				
			||||||
 | 
					    // analysis of the actual commands.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return isset($read_only[$command]);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@@ -30,7 +30,7 @@ final class DiffusionGitResponse extends AphrontResponse {
 | 
				
			|||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  public function getHeaders() {
 | 
					  public function getHeaders() {
 | 
				
			||||||
    return $this->headers;
 | 
					    return array_merge(parent::getHeaders(), $this->headers);
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  public function getCacheHeaders() {
 | 
					  public function getCacheHeaders() {
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -0,0 +1,31 @@
 | 
				
			|||||||
 | 
					<?php
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					final class DiffusionMercurialResponse extends AphrontResponse {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  private $content;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  public function setContent($content) {
 | 
				
			||||||
 | 
					    $this->content = $content;
 | 
				
			||||||
 | 
					    return $this;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  public function buildResponseString() {
 | 
				
			||||||
 | 
					    return $this->content;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  public function getHeaders() {
 | 
				
			||||||
 | 
					    $headers = array(
 | 
				
			||||||
 | 
					      array('Content-Type', 'application/mercurial-0.1'),
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					    return array_merge(parent::getHeaders(), $headers);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  public function getCacheHeaders() {
 | 
				
			||||||
 | 
					    return array();
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  public function getHTTPResponseCode() {
 | 
				
			||||||
 | 
					    return 200;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
		Reference in New Issue
	
	Block a user