Page Menu
Home
Phorge
Search
Configure Global Search
Log In
Files
F2896056
No One
Temporary
Actions
View File
Edit File
Delete File
View Transforms
Subscribe
Award Token
Flag For Later
Advanced/Developer...
View Handle
View Hovercard
Size
13 KB
Referenced Files
None
Subscribers
None
View Options
diff --git a/src/applications/diffusion/ssh/DiffusionGitUploadPackSSHWorkflow.php b/src/applications/diffusion/ssh/DiffusionGitUploadPackSSHWorkflow.php
index 33b21b2ffe..754bafd850 100644
--- a/src/applications/diffusion/ssh/DiffusionGitUploadPackSSHWorkflow.php
+++ b/src/applications/diffusion/ssh/DiffusionGitUploadPackSSHWorkflow.php
@@ -1,66 +1,89 @@
<?php
final class DiffusionGitUploadPackSSHWorkflow extends DiffusionGitSSHWorkflow {
protected function didConstruct() {
$this->setName('git-upload-pack');
$this->setArguments(
array(
array(
'name' => 'dir',
'wildcard' => true,
),
));
}
protected function executeRepositoryOperations() {
$repository = $this->getRepository();
$viewer = $this->getUser();
$device = AlmanacKeys::getLiveDevice();
$skip_sync = $this->shouldSkipReadSynchronization();
+ $is_proxy = $this->shouldProxy();
- if ($this->shouldProxy()) {
+ if ($is_proxy) {
$command = $this->getProxyCommand();
if ($device) {
$this->writeClusterEngineLogMessage(
pht(
"# Fetch received by \"%s\", forwarding to cluster host.\n",
$device->getName()));
}
} else {
$command = csprintf('git-upload-pack -- %s', $repository->getLocalPath());
if (!$skip_sync) {
$cluster_engine = id(new DiffusionRepositoryClusterEngine())
->setViewer($viewer)
->setRepository($repository)
->setLog($this)
->synchronizeWorkingCopyBeforeRead();
if ($device) {
$this->writeClusterEngineLogMessage(
pht(
"# Cleared to fetch on cluster host \"%s\".\n",
$device->getName()));
}
}
}
$command = PhabricatorDaemon::sudoCommandAsDaemonUser($command);
+ $pull_event = $this->newPullEvent();
+
$future = id(new ExecFuture('%C', $command))
->setEnv($this->getEnvironment());
$err = $this->newPassthruCommand()
->setIOChannel($this->getIOChannel())
->setCommandChannelFromExecFuture($future)
->execute();
+ if ($err) {
+ $pull_event
+ ->setResultType('error')
+ ->setResultCode($err);
+ } else {
+ $pull_event
+ ->setResultType('pull')
+ ->setResultCode(0);
+ }
+
+ // TODO: Currently, when proxying, we do not write a log on the proxy.
+ // Perhaps we should write a "proxy log". This is not very useful for
+ // statistics or auditing, but could be useful for diagnostics. Marking
+ // the proxy logs as proxied (and recording devicePHID on all logs) would
+ // make differentiating between these use cases easier.
+
+ if (!$is_proxy) {
+ $pull_event->save();
+ }
+
if (!$err) {
$this->waitForGitClient();
}
return $err;
}
}
diff --git a/src/applications/diffusion/ssh/DiffusionSSHWorkflow.php b/src/applications/diffusion/ssh/DiffusionSSHWorkflow.php
index 78252fb8d9..20a6a251a7 100644
--- a/src/applications/diffusion/ssh/DiffusionSSHWorkflow.php
+++ b/src/applications/diffusion/ssh/DiffusionSSHWorkflow.php
@@ -1,263 +1,273 @@
<?php
abstract class DiffusionSSHWorkflow extends PhabricatorSSHWorkflow {
private $args;
private $repository;
private $hasWriteAccess;
private $proxyURI;
private $baseRequestPath;
public function getRepository() {
if (!$this->repository) {
throw new Exception(pht('Repository is not available yet!'));
}
return $this->repository;
}
private function setRepository(PhabricatorRepository $repository) {
$this->repository = $repository;
return $this;
}
public function getArgs() {
return $this->args;
}
public function getEnvironment() {
$env = array(
DiffusionCommitHookEngine::ENV_USER => $this->getUser()->getUsername(),
DiffusionCommitHookEngine::ENV_REMOTE_PROTOCOL => 'ssh',
);
- $ssh_client = getenv('SSH_CLIENT');
- if ($ssh_client) {
- // This has the format "<ip> <remote-port> <local-port>". Grab the IP.
- $remote_address = head(explode(' ', $ssh_client));
+ $remote_address = $this->getSSHRemoteAddress();
+ if ($remote_address !== null) {
$env[DiffusionCommitHookEngine::ENV_REMOTE_ADDRESS] = $remote_address;
}
return $env;
}
/**
* Identify and load the affected repository.
*/
abstract protected function identifyRepository();
abstract protected function executeRepositoryOperations();
protected function getBaseRequestPath() {
return $this->baseRequestPath;
}
protected function writeError($message) {
$this->getErrorChannel()->write($message);
return $this;
}
protected function getCurrentDeviceName() {
$device = AlmanacKeys::getLiveDevice();
if ($device) {
return $device->getName();
}
return php_uname('n');
}
protected function getTargetDeviceName() {
// TODO: This should use the correct device identity.
$uri = new PhutilURI($this->proxyURI);
return $uri->getDomain();
}
protected function shouldProxy() {
return (bool)$this->proxyURI;
}
protected function getProxyCommand() {
$uri = new PhutilURI($this->proxyURI);
$username = AlmanacKeys::getClusterSSHUser();
if ($username === null) {
throw new Exception(
pht(
'Unable to determine the username to connect with when trying '.
'to proxy an SSH request within the Phabricator cluster.'));
}
$port = $uri->getPort();
$host = $uri->getDomain();
$key_path = AlmanacKeys::getKeyPath('device.key');
if (!Filesystem::pathExists($key_path)) {
throw new Exception(
pht(
'Unable to proxy this SSH request within the cluster: this device '.
'is not registered and has a missing device key (expected to '.
'find key at "%s").',
$key_path));
}
$options = array();
$options[] = '-o';
$options[] = 'StrictHostKeyChecking=no';
$options[] = '-o';
$options[] = 'UserKnownHostsFile=/dev/null';
// This is suppressing "added <address> to the list of known hosts"
// messages, which are confusing and irrelevant when they arise from
// proxied requests. It might also be suppressing lots of useful errors,
// of course. Ideally, we would enforce host keys eventually.
$options[] = '-o';
$options[] = 'LogLevel=quiet';
// NOTE: We prefix the command with "@username", which the far end of the
// connection will parse in order to act as the specified user. This
// behavior is only available to cluster requests signed by a trusted
// device key.
return csprintf(
'ssh %Ls -l %s -i %s -p %s %s -- %s %Ls',
$options,
$username,
$key_path,
$port,
$host,
'@'.$this->getUser()->getUsername(),
$this->getOriginalArguments());
}
final public function execute(PhutilArgumentParser $args) {
$this->args = $args;
$viewer = $this->getUser();
$have_diffusion = PhabricatorApplication::isClassInstalledForViewer(
'PhabricatorDiffusionApplication',
$viewer);
if (!$have_diffusion) {
throw new Exception(
pht(
'You do not have permission to access the Diffusion application, '.
'so you can not interact with repositories over SSH.'));
}
$repository = $this->identifyRepository();
$this->setRepository($repository);
$is_cluster_request = $this->getIsClusterRequest();
$uri = $repository->getAlmanacServiceURI(
$viewer,
$is_cluster_request,
array(
'ssh',
));
if ($uri) {
$this->proxyURI = $uri;
}
try {
return $this->executeRepositoryOperations();
} catch (Exception $ex) {
$this->writeError(get_class($ex).': '.$ex->getMessage());
return 1;
}
}
protected function loadRepositoryWithPath($path) {
$viewer = $this->getUser();
$info = PhabricatorRepository::parseRepositoryServicePath($path);
if ($info === null) {
throw new Exception(
pht(
'Unrecognized repository path "%s". Expected a path like "%s" '.
'or "%s".',
$path,
'/diffusion/X/',
'/diffusion/123/'));
}
$identifier = $info['identifier'];
$base = $info['base'];
$this->baseRequestPath = $base;
$repository = id(new PhabricatorRepositoryQuery())
->setViewer($viewer)
->withIdentifiers(array($identifier))
->needURIs(true)
->executeOne();
if (!$repository) {
throw new Exception(
pht('No repository "%s" exists!', $identifier));
}
$protocol = PhabricatorRepositoryURI::BUILTIN_PROTOCOL_SSH;
if (!$repository->canServeProtocol($protocol, false)) {
throw new Exception(
pht(
'This repository ("%s") is not available over SSH.',
$repository->getDisplayName()));
}
return $repository;
}
protected function requireWriteAccess($protocol_command = null) {
if ($this->hasWriteAccess === true) {
return;
}
$repository = $this->getRepository();
$viewer = $this->getUser();
if ($viewer->isOmnipotent()) {
throw new Exception(
pht(
'This request is authenticated as a cluster device, but is '.
'performing a write. Writes must be performed with a real '.
'user account.'));
}
$protocol = PhabricatorRepositoryURI::BUILTIN_PROTOCOL_SSH;
if ($repository->canServeProtocol($protocol, true)) {
$can_push = PhabricatorPolicyFilter::hasCapability(
$viewer,
$repository,
DiffusionPushCapability::CAPABILITY);
if (!$can_push) {
throw new Exception(
pht('You do not have permission to push to this repository.'));
}
} else {
if ($protocol_command !== null) {
throw new Exception(
pht(
'This repository is read-only over SSH (tried to execute '.
'protocol command "%s").',
$protocol_command));
} else {
throw new Exception(
pht('This repository is read-only over SSH.'));
}
}
$this->hasWriteAccess = true;
return $this->hasWriteAccess;
}
protected function shouldSkipReadSynchronization() {
$viewer = $this->getUser();
// Currently, the only case where devices interact over SSH without
// assuming user credentials is when synchronizing before a read. These
// synchronizing reads do not themselves need to be synchronized.
if ($viewer->isOmnipotent()) {
return true;
}
return false;
}
+ protected function newPullEvent() {
+ $viewer = $this->getViewer();
+ $repository = $this->getRepository();
+ $remote_address = $this->getSSHRemoteAddress();
+
+ return id(new PhabricatorRepositoryPullEvent())
+ ->setEpoch(PhabricatorTime::getNow())
+ ->setRemoteAddress($remote_address)
+ ->setRemoteProtocol('ssh')
+ ->setPullerPHID($viewer->getPHID())
+ ->setRepositoryPHID($repository->getPHID());
+ }
}
diff --git a/src/infrastructure/ssh/PhabricatorSSHWorkflow.php b/src/infrastructure/ssh/PhabricatorSSHWorkflow.php
index b5ac17b7bf..beee356951 100644
--- a/src/infrastructure/ssh/PhabricatorSSHWorkflow.php
+++ b/src/infrastructure/ssh/PhabricatorSSHWorkflow.php
@@ -1,86 +1,101 @@
<?php
abstract class PhabricatorSSHWorkflow extends PhabricatorManagementWorkflow {
private $user;
private $iochannel;
private $errorChannel;
private $isClusterRequest;
private $originalArguments;
public function isExecutable() {
return false;
}
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;
return $this;
}
public function getUser() {
return $this->user;
}
public function setIOChannel(PhutilChannel $channel) {
$this->iochannel = $channel;
return $this;
}
public function getIOChannel() {
return $this->iochannel;
}
public function readAllInput() {
$channel = $this->getIOChannel();
while ($channel->update()) {
PhutilChannel::waitForAny(array($channel));
if (!$channel->isOpenForReading()) {
break;
}
}
return $channel->read();
}
public function writeIO($data) {
$this->getIOChannel()->write($data);
return $this;
}
public function writeErrorIO($data) {
$this->getErrorChannel()->write($data);
return $this;
}
protected function newPassthruCommand() {
return id(new PhabricatorSSHPassthruCommand())
->setErrorChannel($this->getErrorChannel());
}
public function setIsClusterRequest($is_cluster_request) {
$this->isClusterRequest = $is_cluster_request;
return $this;
}
public function getIsClusterRequest() {
return $this->isClusterRequest;
}
public function setOriginalArguments(array $original_arguments) {
$this->originalArguments = $original_arguments;
return $this;
}
public function getOriginalArguments() {
return $this->originalArguments;
}
+ public function getSSHRemoteAddress() {
+ $ssh_client = getenv('SSH_CLIENT');
+ if (!strlen($ssh_client)) {
+ return null;
+ }
+
+ // TODO: When commands are proxied, the original remote address should
+ // also be proxied.
+
+ // This has the format "<ip> <remote-port> <local-port>". Grab the IP.
+ $remote_address = head(explode(' ', $ssh_client));
+
+ return $remote_address;
+ }
+
}
File Metadata
Details
Attached
Mime Type
text/x-diff
Expires
Jan 19 2025, 22:28 (6 w, 3 d ago)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
1129406
Default Alt Text
(13 KB)
Attached To
Mode
rP Phorge
Attached
Detach File
Event Timeline
Log In to Comment