diff --git a/bin/ssh-auth b/bin/ssh-auth new file mode 120000 index 0000000000..136faff7e6 --- /dev/null +++ b/bin/ssh-auth @@ -0,0 +1 @@ +../scripts/ssh/ssh-auth.php \ No newline at end of file diff --git a/bin/ssh-exec b/bin/ssh-exec new file mode 120000 index 0000000000..9d3f453bee --- /dev/null +++ b/bin/ssh-exec @@ -0,0 +1 @@ +../scripts/ssh/ssh-exec.php \ No newline at end of file diff --git a/scripts/conduit/api.php b/scripts/conduit/api.php deleted file mode 100644 index 583b1fdad9..0000000000 --- a/scripts/conduit/api.php +++ /dev/null @@ -1,79 +0,0 @@ -#!/usr/bin/env php - \n"; - exit(1); -} - -$user = null; -$user_str = $argv[1]; -try { - $user = id(new PhabricatorUser()) - ->loadOneWhere('phid = %s', $user_str); -} catch (Exception $e) { - // no op; we'll error in a line or two -} -if (empty($user)) { - echo "usage: api.php \n" . - "user {$user_str} does not exist or failed to load\n"; - exit(1); -} - -$method = $argv[2]; -$method_class_str = ConduitAPIMethod::getClassNameFromAPIMethodName($method); -try { - $method_class = newv($method_class_str, array()); -} catch (Exception $e) { - echo "usage: api.php \n" . - "method {$method_class_str} does not exist\n"; - exit(1); -} -$log = new PhabricatorConduitMethodCallLog(); -$log->setMethod($method); - -$params = @file_get_contents('php://stdin'); -$params = json_decode($params, true); -if (!is_array($params)) { - echo "provide method parameters on stdin as a JSON blob"; - exit(1); -} - -// build a quick ConduitAPIRequest from stdin PLUS the authenticated user -$conduit_request = new ConduitAPIRequest($params); -$conduit_request->setUser($user); - -try { - $result = $method_class->executeMethod($conduit_request); - $error_code = null; - $error_info = null; -} catch (ConduitException $ex) { - $result = null; - $error_code = $ex->getMessage(); - if ($ex->getErrorDescription()) { - $error_info = $ex->getErrorDescription(); - } else { - $error_info = $method_handler->getErrorDescription($error_code); - } -} -$time_end = microtime(true); - -$response = id(new ConduitAPIResponse()) - ->setResult($result) - ->setErrorCode($error_code) - ->setErrorInfo($error_info); -echo json_encode($response->toDictionary()), "\n"; - -// TODO -- how get $connection_id from SSH? -$connection_id = null; -$log->setConnectionID($connection_id); -$log->setError((string)$error_code); -$log->setDuration(1000000 * ($time_end - $time_start)); -$log->save(); - -exit(); diff --git a/scripts/ssh/ssh-auth.php b/scripts/ssh/ssh-auth.php new file mode 100755 index 0000000000..b1fdb246aa --- /dev/null +++ b/scripts/ssh/ssh-auth.php @@ -0,0 +1,54 @@ +#!/usr/bin/env php +establishConnection('r'); + + list($type, $body) = array_merge( + explode(' ', $cert), + array('', '')); + + $row = queryfx_one( + $conn, + 'SELECT userName FROM %T u JOIN %T ssh ON u.phid = ssh.userPHID + WHERE ssh.keyBody = %s AND ssh.keyType = %s', + $user_dao->getTableName(), + $ssh_dao->getTableName(), + $body, + $type); + if ($row) { + $user = idx($row, 'userName'); + } +} + +if (!$user) { + exit(1); +} + +if (!PhabricatorUser::validateUsername($user)) { + exit(1); +} + +$bin = $root.'/bin/ssh-exec'; +$cmd = csprintf('%s --phabricator-ssh-user %s', $bin, $user); +// This is additional escaping for the SSH 'command="..."' string. +$cmd = str_replace('"', '\\"', $cmd); + +$options = array( + 'command="'.$cmd.'"', + 'no-port-forwarding', + 'no-X11-forwarding', + 'no-agent-forwarding', + 'no-pty', +); + +echo implode(',', $options); +exit(0); diff --git a/scripts/ssh/ssh-exec.php b/scripts/ssh/ssh-exec.php new file mode 100755 index 0000000000..47e35632c2 --- /dev/null +++ b/scripts/ssh/ssh-exec.php @@ -0,0 +1,87 @@ +#!/usr/bin/env php +setTagline('receive SSH requests'); +$args->setSynopsis(<<parsePartial( + array( + array( + 'name' => 'phabricator-ssh-user', + 'param' => 'username', + ), + )); + +try { + $user_name = $args->getArg('phabricator-ssh-user'); + if (!strlen($user_name)) { + throw new Exception("No username."); + } + + $user = id(new PhabricatorUser())->loadOneWhere( + 'userName = %s', + $user_name); + if (!$user) { + throw new Exception("Invalid username."); + } + + if ($user->getIsDisabled()) { + throw new Exception("You have been exiled."); + } + + $workflows = array( + new ConduitSSHWorkflow(), + ); + + // This duplicates logic in parseWorkflows(), but allows us to raise more + // concise/relevant exceptions when the client is a remote SSH. + $remain = $args->getUnconsumedArgumentVector(); + if (empty($remain)) { + throw new Exception("No command."); + } else { + $command = head($remain); + $workflow_names = mpull($workflows, 'getName', 'getName'); + if (empty($workflow_names[$command])) { + throw new Exception("Invalid command."); + } + } + + $workflow = $args->parseWorkflows($workflows); + $workflow->setUser($user); + + $sock_stdin = fopen('php://stdin', 'r'); + if (!$sock_stdin) { + throw new Exception("Unable to open stdin."); + } + + $sock_stdout = fopen('php://stdout', 'w'); + if (!$sock_stdout) { + throw new Exception("Unable to open stdout."); + } + + $socket_channel = new PhutilSocketChannel( + $sock_stdin, + $sock_stdout); + $metrics_channel = new PhutilMetricsChannel($socket_channel); + $workflow->setIOChannel($metrics_channel); + + $err = $workflow->execute($args); + + $metrics_channel->flush(); +} catch (Exception $ex) { + echo "phabricator-ssh-exec: ".$ex->getMessage()."\n"; + exit(1); +} diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php index d196470569..1658f92b78 100644 --- a/src/__phutil_library_map__.php +++ b/src/__phutil_library_map__.php @@ -193,6 +193,7 @@ phutil_register_library_map(array( 'ConduitCall' => 'applications/conduit/call/ConduitCall.php', 'ConduitCallTestCase' => 'applications/conduit/call/__tests__/ConduitCallTestCase.php', 'ConduitException' => 'applications/conduit/protocol/ConduitException.php', + 'ConduitSSHWorkflow' => 'applications/conduit/ssh/ConduitSSHWorkflow.php', 'DarkConsoleConfigPlugin' => 'aphront/console/plugin/DarkConsoleConfigPlugin.php', 'DarkConsoleController' => 'aphront/console/DarkConsoleController.php', 'DarkConsoleCore' => 'aphront/console/DarkConsoleCore.php', @@ -1074,6 +1075,7 @@ phutil_register_library_map(array( 'PhabricatorRequestOverseer' => 'infrastructure/PhabricatorRequestOverseer.php', 'PhabricatorS3FileStorageEngine' => 'applications/files/engine/PhabricatorS3FileStorageEngine.php', 'PhabricatorSQLPatchList' => 'infrastructure/storage/patch/PhabricatorSQLPatchList.php', + 'PhabricatorSSHWorkflow' => 'infrastructure/ssh/PhabricatorSSHWorkflow.php', 'PhabricatorScopedEnv' => 'infrastructure/PhabricatorScopedEnv.php', 'PhabricatorSearchAbstractDocument' => 'applications/search/index/PhabricatorSearchAbstractDocument.php', 'PhabricatorSearchAttachController' => 'applications/search/controller/PhabricatorSearchAttachController.php', @@ -1521,6 +1523,7 @@ phutil_register_library_map(array( 'ConduitAPI_user_whoami_Method' => 'ConduitAPI_user_Method', 'ConduitCallTestCase' => 'PhabricatorTestCase', 'ConduitException' => 'Exception', + 'ConduitSSHWorkflow' => 'PhabricatorSSHWorkflow', 'DarkConsoleConfigPlugin' => 'DarkConsolePlugin', 'DarkConsoleController' => 'PhabricatorController', 'DarkConsoleErrorLogPlugin' => 'DarkConsolePlugin', @@ -2337,6 +2340,7 @@ phutil_register_library_map(array( 'PhabricatorRepositorySymbol' => 'PhabricatorRepositoryDAO', 'PhabricatorRepositoryTestCase' => 'PhabricatorTestCase', 'PhabricatorS3FileStorageEngine' => 'PhabricatorFileStorageEngine', + 'PhabricatorSSHWorkflow' => 'PhutilArgumentWorkflow', 'PhabricatorSearchAttachController' => 'PhabricatorSearchBaseController', 'PhabricatorSearchBaseController' => 'PhabricatorController', 'PhabricatorSearchCommitIndexer' => 'PhabricatorSearchDocumentIndexer', diff --git a/src/applications/conduit/ssh/ConduitSSHWorkflow.php b/src/applications/conduit/ssh/ConduitSSHWorkflow.php new file mode 100644 index 0000000000..a7eb4ba916 --- /dev/null +++ b/src/applications/conduit/ssh/ConduitSSHWorkflow.php @@ -0,0 +1,81 @@ +setName('conduit'); + $this->setArguments( + array( + array( + 'name' => 'method', + 'wildcard' => true, + ), + )); + } + + public function execute(PhutilArgumentParser $args) { + $time_start = microtime(true); + + $methodv = $args->getArg('method'); + if (!$methodv) { + throw new Exception("No Conduit method provided."); + } else if (count($methodv) > 1) { + throw new Exception("Too many Conduit methods provided."); + } + + $method = head($methodv); + + $json = $this->readAllInput(); + $raw_params = json_decode($json, true); + if (!is_array($raw_params)) { + throw new Exception("Invalid JSON input."); + } + + $params = $raw_params; + unset($params['__conduit__']); + $metadata = idx($raw_params, '__conduit__', array()); + + $call = null; + $error_code = null; + $error_info = null; + + try { + $call = new ConduitCall($method, $params); + $call->setUser($this->getUser()); + + $result = $call->execute(); + } catch (ConduitException $ex) { + $result = null; + $error_code = $ex->getMessage(); + if ($ex->getErrorDescription()) { + $error_info = $ex->getErrorDescription(); + } else if ($call) { + $error_info = $call->getErrorDescription($error_code); + } + } + + $response = id(new ConduitAPIResponse()) + ->setResult($result) + ->setErrorCode($error_code) + ->setErrorInfo($error_info); + + $json_out = json_encode($response->toDictionary()); + $json_out = $json_out."\n"; + + $this->getIOChannel()->write($json_out); + + // NOTE: Flush here so we can get an accurate result for the duration, + // if the response is large and the receiver is slow to read it. + $this->getIOChannel()->flush(); + + $time_end = microtime(true); + + $connection_id = idx($metadata, 'connectionID'); + $log = new PhabricatorConduitMethodCallLog(); + $log->setConnectionID($connection_id); + $log->setMethod($method); + $log->setError((string)$error_code); + $log->setDuration(1000000 * ($time_end - $time_start)); + $log->save(); + } +} diff --git a/src/infrastructure/ssh/PhabricatorSSHWorkflow.php b/src/infrastructure/ssh/PhabricatorSSHWorkflow.php new file mode 100644 index 0000000000..7ad956c636 --- /dev/null +++ b/src/infrastructure/ssh/PhabricatorSSHWorkflow.php @@ -0,0 +1,41 @@ +user = $user; + return $this; + } + + public function getUser() { + return $this->user; + } + + final public function isExecutable() { + return false; + } + + 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(); + } + +}