Page MenuHomePhabricator

D7551.diff
No OneTemporary

D7551.diff

Index: src/__phutil_library_map__.php
===================================================================
--- src/__phutil_library_map__.php
+++ src/__phutil_library_map__.php
@@ -1746,6 +1746,7 @@
'PhabricatorRepositoryVCSPassword' => 'applications/repository/storage/PhabricatorRepositoryVCSPassword.php',
'PhabricatorS3FileStorageEngine' => 'applications/files/engine/PhabricatorS3FileStorageEngine.php',
'PhabricatorSQLPatchList' => 'infrastructure/storage/patch/PhabricatorSQLPatchList.php',
+ 'PhabricatorSSHPassthruCommand' => 'infrastructure/ssh/PhabricatorSSHPassthruCommand.php',
'PhabricatorSSHWorkflow' => 'infrastructure/ssh/PhabricatorSSHWorkflow.php',
'PhabricatorSavedQuery' => 'applications/search/storage/PhabricatorSavedQuery.php',
'PhabricatorSavedQueryQuery' => 'applications/search/query/PhabricatorSavedQueryQuery.php',
@@ -4175,6 +4176,7 @@
'PhabricatorRepositoryTransactionQuery' => 'PhabricatorApplicationTransactionQuery',
'PhabricatorRepositoryVCSPassword' => 'PhabricatorRepositoryDAO',
'PhabricatorS3FileStorageEngine' => 'PhabricatorFileStorageEngine',
+ 'PhabricatorSSHPassthruCommand' => 'Phobject',
'PhabricatorSSHWorkflow' => 'PhutilArgumentWorkflow',
'PhabricatorSavedQuery' =>
array(
Index: src/applications/diffusion/ssh/DiffusionSSHGitReceivePackWorkflow.php
===================================================================
--- src/applications/diffusion/ssh/DiffusionSSHGitReceivePackWorkflow.php
+++ src/applications/diffusion/ssh/DiffusionSSHGitReceivePackWorkflow.php
@@ -14,10 +14,6 @@
));
}
- public function isReadOnly() {
- return false;
- }
-
public function getRequestPath() {
$args = $this->getArgs();
return head($args->getArg('dir'));
@@ -25,10 +21,17 @@
protected function executeRepositoryOperations(
PhabricatorRepository $repository) {
+
+ // This is a write, and must have write access.
+ $this->requireWriteAccess();
+
$future = new ExecFuture(
'git-receive-pack %s',
$repository->getLocalPath());
- $err = $this->passthruIO($future);
+ $err = $this->newPassthruCommand()
+ ->setIOChannel($this->getIOChannel())
+ ->setCommandChannelFromExecFuture($future)
+ ->execute();
if (!$err) {
$repository->writeStatusMessage(
Index: src/applications/diffusion/ssh/DiffusionSSHGitUploadPackWorkflow.php
===================================================================
--- src/applications/diffusion/ssh/DiffusionSSHGitUploadPackWorkflow.php
+++ src/applications/diffusion/ssh/DiffusionSSHGitUploadPackWorkflow.php
@@ -14,10 +14,6 @@
));
}
- public function isReadOnly() {
- return true;
- }
-
public function getRequestPath() {
$args = $this->getArgs();
return head($args->getArg('dir'));
@@ -28,7 +24,10 @@
$future = new ExecFuture('git-upload-pack %s', $repository->getLocalPath());
- return $this->passthruIO($future);
+ return $this->newPassthruCommand()
+ ->setIOChannel($this->getIOChannel())
+ ->setCommandChannelFromExecFuture($future)
+ ->execute();
}
}
Index: src/applications/diffusion/ssh/DiffusionSSHWorkflow.php
===================================================================
--- src/applications/diffusion/ssh/DiffusionSSHWorkflow.php
+++ src/applications/diffusion/ssh/DiffusionSSHWorkflow.php
@@ -3,12 +3,17 @@
abstract class DiffusionSSHWorkflow extends PhabricatorSSHWorkflow {
private $args;
+ private $repository;
+ private $hasWriteAccess;
+
+ public function getRepository() {
+ return $this->repository;
+ }
public function getArgs() {
return $this->args;
}
- abstract protected function isReadOnly();
abstract protected function getRequestPath();
abstract protected function executeRepositoryOperations(
PhabricatorRepository $repository);
@@ -23,6 +28,7 @@
try {
$repository = $this->loadRepository();
+ $this->repository = $repository;
return $this->executeRepositoryOperations($repository);
} catch (Exception $ex) {
$this->writeError(get_class($ex).': '.$ex->getMessage());
@@ -56,34 +62,55 @@
pht('No repository "%s" exists!', $callsign));
}
- $is_push = !$this->isReadOnly();
+ switch ($repository->getServeOverSSH()) {
+ case PhabricatorRepository::SERVE_READONLY:
+ case PhabricatorRepository::SERVE_READWRITE:
+ // If we have read or read/write access, proceed for now. We will
+ // check write access when the user actually issues a write command.
+ break;
+ case PhabricatorRepository::SERVE_OFF:
+ default:
+ throw new Exception(
+ pht('This repository is not available over SSH.'));
+ }
+
+ return $repository;
+ }
+
+ protected function requireWriteAccess() {
+ if ($this->hasWriteAccess === true) {
+ return;
+ }
+
+ $repository = $this->getRepository();
+ $viewer = $this->getUser();
switch ($repository->getServeOverSSH()) {
case PhabricatorRepository::SERVE_READONLY:
- if ($is_push) {
- throw new Exception(
- pht('This repository is read-only over SSH.'));
- }
+ 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.'));
- }
+ $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:
+ // This shouldn't be reachable because we don't get this far if the
+ // repository isn't enabled, but kick them out anyway.
throw new Exception(
pht('This repository is not available over SSH.'));
}
- return $repository;
+ $this->hasWriteAccess = true;
+ return $this->hasWriteAccess;
}
+
}
Index: src/infrastructure/ssh/PhabricatorSSHPassthruCommand.php
===================================================================
--- /dev/null
+++ src/infrastructure/ssh/PhabricatorSSHPassthruCommand.php
@@ -0,0 +1,161 @@
+<?php
+
+/**
+ * Proxy an IO channel to an underlying command, with optional callbacks. This
+ * is a mostly a more general version of @{class:PhutilExecPassthru}. This
+ * class is used to proxy Git, SVN and Mercurial traffic to the commands which
+ * can actually serve it.
+ *
+ * Largely, this just reads an IO channel (like stdin from SSH) and writes
+ * the results into a command channel (like a command's stdin). Then it reads
+ * the command channel (like the command's stdout) and writes it into the IO
+ * channel (like stdout from SSH):
+ *
+ * IO Channel Command Channel
+ * stdin -> stdin
+ * stdout <- stdout
+ * stderr <- stderr
+ *
+ * You can provide **read and write callbacks** which are invoked as data
+ * is passed through this class. They allow you to inspect and modify traffic.
+ *
+ * IO Channel Passthru Command Channel
+ * stdout -> willWrite -> stdin
+ * stdin <- willRead <- stdout
+ * stderr <- (identity) <- stderr
+ *
+ * Primarily, this means:
+ *
+ * - the **IO Channel** can be a @{class:PhutilProtocolChannel} if the
+ * **write callback** can convert protocol messages into strings; and
+ * - the **write callback** can inspect and reject requests over the channel,
+ * e.g. to enforce policies.
+ *
+ * In practice, this is used when serving repositories to check each command
+ * issued over SSH and determine if it is a read command or a write command.
+ * Writes can then be checked for appropriate permissions.
+ */
+final class PhabricatorSSHPassthruCommand extends Phobject {
+
+ private $commandChannel;
+ private $ioChannel;
+ private $errorChannel;
+ private $execFuture;
+ private $willWriteCallback;
+ private $willReadCallback;
+
+ public function setCommandChannelFromExecFuture(ExecFuture $exec_future) {
+ $exec_channel = new PhutilExecChannel($exec_future);
+ $exec_channel->setStderrHandler(array($this, 'writeErrorIOCallback'));
+
+ $this->execFuture = $exec_future;
+ $this->commandChannel = $exec_channel;
+
+ return $this;
+ }
+
+ public function setIOChannel(PhutilChannel $io_channel) {
+ $this->ioChannel = $io_channel;
+ return $this;
+ }
+
+ public function setErrorChannel(PhutilChannel $error_channel) {
+ $this->errorChannel = $error_channel;
+ return $this;
+ }
+
+ public function setWillReadCallback($will_read_callback) {
+ $this->willReadCallback = $will_read_callback;
+ return $this;
+ }
+
+ public function setWillWriteCallback($will_write_callback) {
+ $this->willWriteCallback = $will_write_callback;
+ return $this;
+ }
+
+ public function writeErrorIOCallback(PhutilChannel $channel, $data) {
+ $this->errorChannel->write($data);
+ }
+
+ public function execute() {
+ $command_channel = $this->commandChannel;
+ $io_channel = $this->ioChannel;
+ $error_channel = $this->errorChannel;
+
+ if (!$command_channel) {
+ throw new Exception("Set a command channel before calling execute()!");
+ }
+
+ if (!$io_channel) {
+ throw new Exception("Set an IO channel before calling execute()!");
+ }
+
+ if (!$error_channel) {
+ throw new Exception("Set an error channel before calling execute()!");
+ }
+
+ $channels = array($command_channel, $io_channel, $error_channel);
+
+ while (true) {
+ PhutilChannel::waitForAny($channels);
+
+ $io_channel->update();
+ $command_channel->update();
+ $error_channel->update();
+
+ $done = !$command_channel->isOpen();
+
+ $in_message = $io_channel->read();
+ $in_message = $this->willWriteData($in_message);
+ if ($in_message !== null) {
+ $command_channel->write($in_message);
+ }
+
+ $out_message = $command_channel->read();
+ $out_message = $this->willReadData($out_message);
+ if ($out_message !== null) {
+ $io_channel->write($out_message);
+ }
+
+ // If we have nothing left on stdin, close stdin on the subprocess.
+ if (!$io_channel->isOpenForReading()) {
+ // TODO: This should probably be part of PhutilExecChannel?
+ $this->execFuture->write('');
+ }
+
+ if ($done) {
+ break;
+ }
+ }
+
+ list($err) = $this->execFuture->resolve();
+
+ return $err;
+ }
+
+ public function willWriteData($message) {
+ if ($this->willWriteCallback) {
+ return call_user_func($this->willWriteCallback, $this, $message);
+ } else {
+ if (strlen($message)) {
+ return $message;
+ } else {
+ return null;
+ }
+ }
+ }
+
+ public function willReadData($message) {
+ if ($this->willReadCallback) {
+ return call_user_func($this->willReadCallback, $this, $message);
+ } else {
+ if (strlen($message)) {
+ return $message;
+ } else {
+ return null;
+ }
+ }
+ }
+
+}
Index: src/infrastructure/ssh/PhabricatorSSHWorkflow.php
===================================================================
--- src/infrastructure/ssh/PhabricatorSSHWorkflow.php
+++ src/infrastructure/ssh/PhabricatorSSHWorkflow.php
@@ -37,50 +37,6 @@
return $this->iochannel;
}
- public function passthruIO(ExecFuture $future) {
- $exec_channel = new PhutilExecChannel($future);
- $exec_channel->setStderrHandler(array($this, 'writeErrorIOCallback'));
-
- $io_channel = $this->getIOChannel();
- $error_channel = $this->getErrorChannel();
-
- $channels = array($exec_channel, $io_channel, $error_channel);
-
- while (true) {
- PhutilChannel::waitForAny($channels);
-
- $io_channel->update();
- $exec_channel->update();
- $error_channel->update();
-
- $done = !$exec_channel->isOpen();
-
- $data = $io_channel->read();
- if (strlen($data)) {
- $exec_channel->write($data);
- }
-
- $data = $exec_channel->read();
- if (strlen($data)) {
- $io_channel->write($data);
- }
-
- // If we have nothing left on stdin, close stdin on the subprocess.
- if (!$io_channel->isOpenForReading()) {
- // TODO: This should probably be part of PhutilExecChannel?
- $future->write('');
- }
-
- if ($done) {
- break;
- }
- }
-
- list($err) = $future->resolve();
-
- return $err;
- }
-
public function readAllInput() {
$channel = $this->getIOChannel();
while ($channel->update()) {
@@ -102,8 +58,9 @@
return $this;
}
- public function writeErrorIOCallback(PhutilChannel $channel, $data) {
- $this->writeErrorIO($data);
+ protected function newPassthruCommand() {
+ return id(new PhabricatorSSHPassthruCommand())
+ ->setErrorChannel($this->getErrorChannel());
}
}

File Metadata

Mime Type
text/plain
Expires
Oct 21 2024, 7:25 AM (4 w, 2 d ago)
Storage Engine
blob
Storage Format
Encrypted (AES-256-CBC)
Storage Handle
6738746
Default Alt Text
D7551.diff (13 KB)

Event Timeline