From bb4904553f2cc16292711be276e89f4eccdaab38 Mon Sep 17 00:00:00 2001 From: epriestley Date: Sat, 26 Oct 2013 10:46:09 -0700 Subject: [PATCH] Route some VCS connections over SSH Summary: - Add web UI for configuring SSH hosting. - Route git reads (`git-upload-pack` over SSH). Test Plan: >>> orbital ~ $ git clone ssh://127.0.0.1/ Cloning into '127.0.0.1'... Exception: Unrecognized repository path "/". Expected a path like "/diffusion/X/". fatal: Could not read from remote repository. Please make sure you have the correct access rights and the repository exists. >>> orbital ~ $ git clone ssh://127.0.0.1/diffusion/X/ Cloning into 'X'... Exception: No repository "X" exists! fatal: Could not read from remote repository. Please make sure you have the correct access rights and the repository exists. >>> orbital ~ $ git clone ssh://127.0.0.1/diffusion/MT/ Cloning into 'MT'... Exception: This repository is not available over SSH. fatal: Could not read from remote repository. Please make sure you have the correct access rights and the repository exists. >>> orbital ~ $ git clone ssh://127.0.0.1/diffusion/P/ Cloning into 'P'... Exception: TODO: Implement serve over SSH. fatal: Could not read from remote repository. Please make sure you have the correct access rights and the repository exists. Reviewers: btrahan Reviewed By: btrahan CC: hach-que, aran Maniphest Tasks: T2230 Differential Revision: https://secure.phabricator.com/D7421 --- scripts/ssh/ssh-exec.php | 12 ++- src/__phutil_library_map__.php | 6 ++ ...ffusionRepositoryEditHostingController.php | 26 +++++- .../ssh/DiffusionSSHGitUploadPackWorkflow.php | 26 ++++++ .../diffusion/ssh/DiffusionSSHGitWorkflow.php | 10 +++ .../diffusion/ssh/DiffusionSSHWorkflow.php | 90 +++++++++++++++++++ .../ssh/PhabricatorSSHWorkflow.php | 15 ++++ 7 files changed, 183 insertions(+), 2 deletions(-) create mode 100644 src/applications/diffusion/ssh/DiffusionSSHGitUploadPackWorkflow.php create mode 100644 src/applications/diffusion/ssh/DiffusionSSHGitWorkflow.php create mode 100644 src/applications/diffusion/ssh/DiffusionSSHWorkflow.php diff --git a/scripts/ssh/ssh-exec.php b/scripts/ssh/ssh-exec.php index 6bd5b71ef3..74d79ed632 100755 --- a/scripts/ssh/ssh-exec.php +++ b/scripts/ssh/ssh-exec.php @@ -61,6 +61,8 @@ try { $workflows = array( new ConduitSSHWorkflow(), + + new DiffusionSSHGitUploadPackWorkflow(), ); $workflow_names = mpull($workflows, 'getName', 'getName'); @@ -81,16 +83,24 @@ try { throw new Exception("Unable to open stdout."); } + $sock_stderr = fopen('php://stderr', 'w'); + if (!$sock_stderr) { + throw new Exception("Unable to open stderr."); + } + $socket_channel = new PhutilSocketChannel( $sock_stdin, $sock_stdout); + $error_channel = new PhutilSocketChannel(null, $sock_stderr); $metrics_channel = new PhutilMetricsChannel($socket_channel); $workflow->setIOChannel($metrics_channel); + $workflow->setErrorChannel($error_channel); $err = $workflow->execute($original_args); $metrics_channel->flush(); + $error_channel->flush(); } catch (Exception $ex) { - echo "phabricator-ssh-exec: ".$ex->getMessage()."\n"; + fwrite(STDERR, "phabricator-ssh-exec: ".$ex->getMessage()."\n"); exit(1); } diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php index 93183e2352..c0f0cd6acf 100644 --- a/src/__phutil_library_map__.php +++ b/src/__phutil_library_map__.php @@ -526,6 +526,9 @@ phutil_register_library_map(array( 'DiffusionRepositoryPath' => 'applications/diffusion/data/DiffusionRepositoryPath.php', 'DiffusionRepositoryTag' => 'applications/diffusion/data/DiffusionRepositoryTag.php', 'DiffusionRequest' => 'applications/diffusion/request/DiffusionRequest.php', + 'DiffusionSSHGitUploadPackWorkflow' => 'applications/diffusion/ssh/DiffusionSSHGitUploadPackWorkflow.php', + 'DiffusionSSHGitWorkflow' => 'applications/diffusion/ssh/DiffusionSSHGitWorkflow.php', + 'DiffusionSSHWorkflow' => 'applications/diffusion/ssh/DiffusionSSHWorkflow.php', 'DiffusionSetupException' => 'applications/diffusion/exception/DiffusionSetupException.php', 'DiffusionStableCommitNameQuery' => 'applications/diffusion/query/stablecommitname/DiffusionStableCommitNameQuery.php', 'DiffusionSvnCommitParentsQuery' => 'applications/diffusion/query/parents/DiffusionSvnCommitParentsQuery.php', @@ -2711,6 +2714,9 @@ phutil_register_library_map(array( 0 => 'DiffusionController', 1 => 'PhabricatorApplicationSearchResultsControllerInterface', ), + 'DiffusionSSHGitUploadPackWorkflow' => 'DiffusionSSHGitWorkflow', + 'DiffusionSSHGitWorkflow' => 'DiffusionSSHWorkflow', + 'DiffusionSSHWorkflow' => 'PhabricatorSSHWorkflow', 'DiffusionSetupException' => 'AphrontUsageException', 'DiffusionStableCommitNameQuery' => 'DiffusionQuery', 'DiffusionSvnCommitParentsQuery' => 'DiffusionCommitParentsQuery', diff --git a/src/applications/diffusion/controller/DiffusionRepositoryEditHostingController.php b/src/applications/diffusion/controller/DiffusionRepositoryEditHostingController.php index 293272e44e..5a47b08d1e 100644 --- a/src/applications/diffusion/controller/DiffusionRepositoryEditHostingController.php +++ b/src/applications/diffusion/controller/DiffusionRepositoryEditHostingController.php @@ -134,7 +134,7 @@ final class DiffusionRepositoryEditHostingController if ($request->isFormPost()) { $v_http_mode = $request->getStr('http'); - $v_ssh_mode = PhabricatorRepository::SERVE_OFF; + $v_ssh_mode = $request->getStr('ssh'); $xactions = array(); $template = id(new PhabricatorRepositoryTransaction()); @@ -176,6 +176,29 @@ final class DiffusionRepositoryEditHostingController 'writes.'); } + $ssh_control = + id(new AphrontFormRadioButtonControl()) + ->setName('ssh') + ->setLabel(pht('SSH')) + ->setValue($v_ssh_mode) + ->addButton( + PhabricatorRepository::SERVE_OFF, + PhabricatorRepository::getProtocolAvailabilityName( + PhabricatorRepository::SERVE_OFF), + pht('Phabricator will not serve this repository.')) + ->addButton( + PhabricatorRepository::SERVE_READONLY, + PhabricatorRepository::getProtocolAvailabilityName( + PhabricatorRepository::SERVE_READONLY), + pht('Phabricator will serve a read-only copy of this repository.')) + ->addButton( + PhabricatorRepository::SERVE_READWRITE, + PhabricatorRepository::getProtocolAvailabilityName( + PhabricatorRepository::SERVE_READWRITE), + $rw_message, + $repository->isHosted() ? null : 'disabled', + $repository->isHosted() ? null : true); + $http_control = id(new AphrontFormRadioButtonControl()) ->setName('http') @@ -205,6 +228,7 @@ final class DiffusionRepositoryEditHostingController pht( 'Phabricator can serve repositories over various protocols. You can '. 'configure server protocols here.')) + ->appendChild($ssh_control) ->appendChild($http_control) ->appendChild( id(new AphrontFormSubmitControl()) diff --git a/src/applications/diffusion/ssh/DiffusionSSHGitUploadPackWorkflow.php b/src/applications/diffusion/ssh/DiffusionSSHGitUploadPackWorkflow.php new file mode 100644 index 0000000000..4dab5f2972 --- /dev/null +++ b/src/applications/diffusion/ssh/DiffusionSSHGitUploadPackWorkflow.php @@ -0,0 +1,26 @@ +setName('git-upload-pack'); + $this->setArguments( + array( + array( + 'name' => 'dir', + 'wildcard' => true, + ), + )); + } + + public function isReadOnly() { + return true; + } + + public function getRequestPath() { + $args = $this->getArgs(); + return head($args->getArg('dir')); + } + +} diff --git a/src/applications/diffusion/ssh/DiffusionSSHGitWorkflow.php b/src/applications/diffusion/ssh/DiffusionSSHGitWorkflow.php new file mode 100644 index 0000000000..ceaf7c146e --- /dev/null +++ b/src/applications/diffusion/ssh/DiffusionSSHGitWorkflow.php @@ -0,0 +1,10 @@ +args; + } + + abstract protected function isReadOnly(); + abstract protected function getRequestPath(); + protected function writeError($message) { + $this->getErrorChannel()->write($message); + return $this; + } + + final public function execute(PhutilArgumentParser $args) { + $this->args = $args; + + try { + $repository = $this->loadRepository(); + + throw new Exception("TODO: Implement serve over SSH."); + + } catch (Exception $ex) { + $this->writeError(get_class($ex).': '.$ex->getMessage()); + return 1; + } + + return 0; + } + + private function loadRepository() { + $viewer = $this->getUser(); + $path = $this->getRequestPath(); + + $regex = '@^/?diffusion/(?P[A-Z]+)(?:/|$)@'; + $matches = null; + if (!preg_match($regex, $path, $matches)) { + throw new Exception( + pht( + 'Unrecognized repository path "%s". Expected a path like '. + '"%s".', + $path, + "/diffusion/X/")); + } + + $callsign = $matches[1]; + $repository = id(new PhabricatorRepositoryQuery()) + ->setViewer($viewer) + ->withCallsigns(array($callsign)) + ->executeOne(); + + if (!$repository) { + throw new Exception( + pht('No repository "%s" exists!', $callsign)); + } + + $is_push = !$this->isReadOnly(); + + switch ($repository->getServeOverSSH()) { + case PhabricatorRepository::SERVE_READONLY: + if ($is_push) { + throw new Exception( + pht('This repository is read-only over SSH.')); + } + break; + case PhabricatorRepository::SERVE_READWRITE: + if ($is_push) { + $can_push = PhabricatorPolicyFilter::hasCapability( + $viewer, + $repository, + DiffusionCapabilityPush::CAPABILITY); + if (!$can_push) { + throw new Exception( + pht('You do not have permission to push to this repository.')); + } + } + break; + case PhabricatorRepository::SERVE_OFF: + default: + throw new Exception( + pht('This repository is not available over SSH.')); + } + + return $repository; + } + +} diff --git a/src/infrastructure/ssh/PhabricatorSSHWorkflow.php b/src/infrastructure/ssh/PhabricatorSSHWorkflow.php index 7ad956c636..9f28d50c0a 100644 --- a/src/infrastructure/ssh/PhabricatorSSHWorkflow.php +++ b/src/infrastructure/ssh/PhabricatorSSHWorkflow.php @@ -4,6 +4,16 @@ abstract class PhabricatorSSHWorkflow extends PhutilArgumentWorkflow { private $user; private $iochannel; + private $errorChannel; + + public function setErrorChannel(PhutilChannel $error_channel) { + $this->errorChannel = $error_channel; + return $this; + } + + public function getErrorChannel() { + return $this->errorChannel; + } public function setUser(PhabricatorUser $user) { $this->user = $user; @@ -38,4 +48,9 @@ abstract class PhabricatorSSHWorkflow extends PhutilArgumentWorkflow { return $channel->read(); } + public function writeIO($data) { + $this->getIOChannel()->write($data); + return $this; + } + }