From 7d9dfb561dd5b76b9289430f11b38d5fbabd2f6f Mon Sep 17 00:00:00 2001 From: epriestley Date: Sat, 26 Oct 2013 12:10:52 -0700 Subject: [PATCH] Serve Git reads over HTTP Summary: Mostly ripped from D7391. No writes yet. Test Plan: Ran `git clone` against a local over HTTP, got a clone. Reviewers: btrahan, hach-que Reviewed By: hach-que CC: aran Maniphest Tasks: T2230 Differential Revision: https://secure.phabricator.com/D7423 --- src/__phutil_library_map__.php | 2 + .../controller/DiffusionController.php | 79 +++++++++++++++++-- .../response/DiffusionGitResponse.php | 44 +++++++++++ 3 files changed, 119 insertions(+), 6 deletions(-) create mode 100644 src/applications/diffusion/response/DiffusionGitResponse.php diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php index c0f0cd6acf..a4d008da0a 100644 --- a/src/__phutil_library_map__.php +++ b/src/__phutil_library_map__.php @@ -480,6 +480,7 @@ phutil_register_library_map(array( 'DiffusionGitFileContentQuery' => 'applications/diffusion/query/filecontent/DiffusionGitFileContentQuery.php', 'DiffusionGitRawDiffQuery' => 'applications/diffusion/query/rawdiff/DiffusionGitRawDiffQuery.php', 'DiffusionGitRequest' => 'applications/diffusion/request/DiffusionGitRequest.php', + 'DiffusionGitResponse' => 'applications/diffusion/response/DiffusionGitResponse.php', 'DiffusionGitStableCommitNameQuery' => 'applications/diffusion/query/stablecommitname/DiffusionGitStableCommitNameQuery.php', 'DiffusionHistoryController' => 'applications/diffusion/controller/DiffusionHistoryController.php', 'DiffusionHistoryTableView' => 'applications/diffusion/view/DiffusionHistoryTableView.php', @@ -2673,6 +2674,7 @@ phutil_register_library_map(array( 'DiffusionGitFileContentQuery' => 'DiffusionFileContentQuery', 'DiffusionGitRawDiffQuery' => 'DiffusionRawDiffQuery', 'DiffusionGitRequest' => 'DiffusionRequest', + 'DiffusionGitResponse' => 'AphrontResponse', 'DiffusionGitStableCommitNameQuery' => 'DiffusionStableCommitNameQuery', 'DiffusionHistoryController' => 'DiffusionController', 'DiffusionHistoryTableView' => 'DiffusionView', diff --git a/src/applications/diffusion/controller/DiffusionController.php b/src/applications/diffusion/controller/DiffusionController.php index 2fb58b32fa..e70319332f 100644 --- a/src/applications/diffusion/controller/DiffusionController.php +++ b/src/applications/diffusion/controller/DiffusionController.php @@ -17,6 +17,8 @@ abstract class DiffusionController extends PhabricatorController { if (preg_match($regex, (string)$uri, $matches)) { $vcs = null; + $content_type = $request->getHTTPHeader('Content-Type'); + if ($request->getExists('__vcs__')) { // This is magic to make it easier for us to debug stuff by telling // users to run: @@ -26,8 +28,13 @@ abstract class DiffusionController extends PhabricatorController { // ...to get a human-readable error. $vcs = $request->getExists('__vcs__'); } else if ($request->getExists('service')) { + $service = $request->getStr('service'); + // We get this initially for `info/refs`. // Git also gives us a User-Agent like "git/1.8.2.3". $vcs = PhabricatorRepositoryType::REPOSITORY_TYPE_GIT; + } else if ($content_type == 'application/x-git-upload-pack-request') { + // We get this for `git-upload-pack`. + $vcs = PhabricatorRepositoryType::REPOSITORY_TYPE_GIT; } else if ($request->getExists('cmd')) { // Mercurial also sends an Accept header like // "application/mercurial-0.1", and a User-Agent like @@ -125,6 +132,13 @@ abstract class DiffusionController extends PhabricatorController { pht('This repository is not available over HTTP.')); } + switch ($repository->getVersionControlSystem()) { + case PhabricatorRepositoryType::REPOSITORY_TYPE_GIT: + return $this->serveGitRequest($repository); + default: + break; + } + return new PhabricatorVCSResponse( 999, pht('TODO: Implement meaningful responses.')); @@ -133,22 +147,28 @@ abstract class DiffusionController extends PhabricatorController { private function isReadOnlyRequest( PhabricatorRepository $repository) { $request = $this->getRequest(); + $method = $_SERVER['REQUEST_METHOD']; // TODO: This implementation is safe by default, but very incomplete. switch ($repository->getVersionControlSystem()) { case PhabricatorRepositoryType::REPOSITORY_TYPE_GIT: $service = $request->getStr('service'); + $path = $this->getRequestDirectoryPath(); // NOTE: Service names are the reverse of what you might expect, as they // are from the point of view of the server. The main read service is // "git-upload-pack", and the main write service is "git-receive-pack". - switch ($service) { - case 'git-upload-pack': - return true; - case 'git-receive-pack': - default: - return false; + + if ($method == 'GET' && + $path == '/info/refs' && + $service == 'git-upload-pack') { + return true; } + + if ($path == '/git-upload-pack') { + return true; + } + break; case PhabricatorRepositoryType::REPOSITORY_TYPE_MERCURIAL: $cmd = $request->getStr('cmd'); @@ -375,5 +395,52 @@ abstract class DiffusionController extends PhabricatorController { return $links; } + /** + * @phutil-external-symbol class PhabricatorStartup + */ + private function serveGitRequest(PhabricatorRepository $repository) { + $request = $this->getRequest(); + + $request_path = $this->getRequestDirectoryPath(); + $repository_root = $repository->getLocalPath(); + + // Rebuild the query string to strip `__magic__` parameters and prevent + // issues where we might interpret inputs like "service=read&service=write" + // differently than the server does and pass it an unsafe command. + $query_data = $request->getPassthroughRequestParameters(); + $query_string = http_build_query($query_data, '', '&'); + + // We're about to wipe out PATH with the rest of the environment, so + // resolve the binary first. + $bin = Filesystem::resolveBinary('git-http-backend'); + if (!$bin) { + throw new Exception("Unable to find `git-http-backend` in PATH!"); + } + + $env = array( + 'REQUEST_METHOD' => $_SERVER['REQUEST_METHOD'], + 'QUERY_STRING' => $query_string, + 'CONTENT_TYPE' => $_SERVER['CONTENT_TYPE'], + 'REMOTE_USER' => '', + 'REMOTE_ADDR' => $_SERVER['REMOTE_ADDR'], + 'GIT_PROJECT_ROOT' => $repository_root, + 'GIT_HTTP_EXPORT_ALL' => '1', + 'PATH_INFO' => $request_path, + ); + + list($stdout) = id(new ExecFuture('%s', $bin)) + ->setEnv($env, true) + ->write(PhabricatorStartup::getRawInput()) + ->resolvex(); + + return id(new DiffusionGitResponse())->setGitData($stdout); + } + + private function getRequestDirectoryPath() { + $request = $this->getRequest(); + $request_path = $request->getRequestURI()->getPath(); + return preg_replace('@^/diffusion/[A-Z]+@', '', $request_path); + } + } diff --git a/src/applications/diffusion/response/DiffusionGitResponse.php b/src/applications/diffusion/response/DiffusionGitResponse.php new file mode 100644 index 0000000000..edf8d4f051 --- /dev/null +++ b/src/applications/diffusion/response/DiffusionGitResponse.php @@ -0,0 +1,44 @@ +response = $body; + $headers = explode("\r\n", $headers); + + $matches = null; + $this->httpCode = 200; + $this->headers = array(); + foreach ($headers as $header) { + if (preg_match('/^Status:\s*(\d+)/i', $header, $matches)) { + $this->httpCode = (int)$matches[1]; + } else { + $this->headers[] = explode(': ', $header, 2); + } + } + + return $this; + } + + public function buildResponseString() { + return $this->response; + } + + public function getHeaders() { + return $this->headers; + } + + public function getCacheHeaders() { + return array(); + } + + public function getHTTPResponseCode() { + return $this->httpCode; + } + +}