Changeset View
Changeset View
Standalone View
Standalone View
scripts/ssh/ssh-exec.php
| #!/usr/bin/env php | #!/usr/bin/env php | ||||
| <?php | <?php | ||||
| $ssh_start_time = microtime(true); | $ssh_start_time = microtime(true); | ||||
| $root = dirname(dirname(dirname(__FILE__))); | $root = dirname(dirname(dirname(__FILE__))); | ||||
| require_once $root.'/scripts/__init_script__.php'; | require_once $root.'/scripts/__init_script__.php'; | ||||
| $ssh_log = PhabricatorSSHLog::getLog(); | $ssh_log = PhabricatorSSHLog::getLog(); | ||||
| // First, figure out the authenticated user. | |||||
| $args = new PhutilArgumentParser($argv); | $args = new PhutilArgumentParser($argv); | ||||
| $args->setTagline('receive SSH requests'); | $args->setTagline('execute SSH requests'); | ||||
| $args->setSynopsis(<<<EOSYNOPSIS | $args->setSynopsis(<<<EOSYNOPSIS | ||||
| **ssh-exec** --phabricator-ssh-user __user__ [--ssh-command __commmand__] | **ssh-exec** --phabricator-ssh-user __user__ [--ssh-command __commmand__] | ||||
| Receive SSH requests. | **ssh-exec** --phabricator-ssh-device __device__ [--ssh-command __commmand__] | ||||
| Execute authenticated SSH requests. This script is normally invoked | |||||
| via SSHD, but can be invoked manually for testing. | |||||
| EOSYNOPSIS | EOSYNOPSIS | ||||
| ); | ); | ||||
| $args->parse( | $args->parse( | ||||
| array( | array( | ||||
| array( | array( | ||||
| 'name' => 'phabricator-ssh-user', | 'name' => 'phabricator-ssh-user', | ||||
| 'param' => 'username', | 'param' => 'username', | ||||
| 'help' => pht( | |||||
| 'If the request authenticated with a user key, the name of the '. | |||||
| 'user.'), | |||||
| ), | |||||
| array( | |||||
| 'name' => 'phabricator-ssh-device', | |||||
| 'param' => 'name', | |||||
| 'help' => pht( | |||||
| 'If the request authenticated with a device key, the name of the '. | |||||
| 'device.'), | |||||
| ), | |||||
| array( | |||||
| 'name' => 'phabricator-ssh-key', | |||||
| 'param' => 'id', | |||||
| 'help' => pht( | |||||
| 'The ID of the SSH key which authenticated this request. This is '. | |||||
| 'used to allow logs to report when specific keys were used, to make '. | |||||
| 'it easier to manage credentials.'), | |||||
| ), | ), | ||||
| array( | array( | ||||
| 'name' => 'ssh-command', | 'name' => 'ssh-command', | ||||
| 'param' => 'command', | 'param' => 'command', | ||||
| 'help' => pht( | |||||
| 'Provide a command to execute. This makes testing this script '. | |||||
| 'easier. When running normally, the command is read from the '. | |||||
| 'environment (SSH_ORIGINAL_COMMAND), which is populated by sshd.'), | |||||
| ), | ), | ||||
| )); | )); | ||||
| try { | try { | ||||
| $remote_address = null; | |||||
| $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)); | |||||
| $ssh_log->setData( | |||||
| array( | |||||
| 'r' => $remote_address, | |||||
| )); | |||||
| } | |||||
| $key_id = $args->getArg('phabricator-ssh-key'); | |||||
| if ($key_id) { | |||||
| $ssh_log->setData( | |||||
| array( | |||||
| 'k' => $key_id, | |||||
| )); | |||||
| } | |||||
| $user_name = $args->getArg('phabricator-ssh-user'); | $user_name = $args->getArg('phabricator-ssh-user'); | ||||
| if (!strlen($user_name)) { | $device_name = $args->getArg('phabricator-ssh-device'); | ||||
| throw new Exception('No username.'); | |||||
| $user = null; | |||||
| $device = null; | |||||
| $is_cluster_request = false; | |||||
| if ($user_name && $device_name) { | |||||
| throw new Exception( | |||||
| pht( | |||||
| 'The --phabricator-ssh-user and --phabricator-ssh-device flags are '. | |||||
| 'mutually exclusive. You can not authenticate as both a user ("%s") '. | |||||
| 'and a device ("%s"). Specify one or the other, but not both.', | |||||
| $user_name, | |||||
| $device_name)); | |||||
| } else if (strlen($user_name)) { | |||||
| $user = id(new PhabricatorPeopleQuery()) | |||||
| ->setViewer(PhabricatorUser::getOmnipotentUser()) | |||||
| ->withUsernames(array($user_name)) | |||||
| ->executeOne(); | |||||
| if (!$user) { | |||||
| throw new Exception( | |||||
| pht( | |||||
| 'Invalid username ("%s"). There is no user with this username.', | |||||
| $user_name)); | |||||
| } | |||||
| } else if (strlen($device_name)) { | |||||
| if (!$remote_address) { | |||||
| throw new Exception( | |||||
| pht( | |||||
| 'Unable to identify remote address from the SSH_CLIENT environment '. | |||||
| 'variable. Device authentication is accepted only from trusted '. | |||||
| 'sources.')); | |||||
| } | |||||
| if (!PhabricatorEnv::isClusterAddress($remote_address)) { | |||||
| throw new Exception( | |||||
| pht( | |||||
| 'This request originates from outside of the Phabricator cluster '. | |||||
| 'address range. Requests signed with a trusted device key must '. | |||||
| 'originate from trusted hosts.')); | |||||
| } | |||||
| $device = id(new AlmanacDeviceQuery()) | |||||
| ->setViewer(PhabricatorUser::getOmnipotentUser()) | |||||
| ->withNames(array($device_name)) | |||||
| ->executeOne(); | |||||
| if (!$device) { | |||||
| throw new Exception( | |||||
| pht( | |||||
| 'Invalid device name ("%s"). There is no device with this name.', | |||||
| $device->getName())); | |||||
| } | } | ||||
| $user = id(new PhabricatorUser())->loadOneWhere( | // We're authenticated as a device, but we're going to read the user out of | ||||
| 'userName = %s', | // the command below. | ||||
| $user_name); | $is_cluster_request = true; | ||||
| } else { | |||||
| throw new Exception( | |||||
| pht( | |||||
| 'This script must be invoked with either the --phabricator-ssh-user '. | |||||
| 'or --phabricator-ssh-device flag.')); | |||||
| } | |||||
| if ($args->getArg('ssh-command')) { | |||||
| $original_command = $args->getArg('ssh-command'); | |||||
| } else { | |||||
| $original_command = getenv('SSH_ORIGINAL_COMMAND'); | |||||
| } | |||||
| $original_argv = id(new PhutilShellLexer()) | |||||
| ->splitArguments($original_command); | |||||
| if ($device) { | |||||
| $act_as_name = array_shift($original_argv); | |||||
| if (!preg_match('/^@/', $act_as_name)) { | |||||
| throw new Exception( | |||||
| pht( | |||||
| 'Commands executed by devices must identify an acting user in the '. | |||||
| 'first command argument. This request was not constructed '. | |||||
| 'properly.')); | |||||
| } | |||||
| $act_as_name = substr($act_as_name, 1); | |||||
epriestley: These variables are junk since we have another `$user` and a `$user_name` vs `$username`, I'll… | |||||
| $user = id(new PhabricatorPeopleQuery()) | |||||
| ->setViewer(PhabricatorUser::getOmnipotentUser()) | |||||
| ->withUsernames(array($act_as_name)) | |||||
| ->executeOne(); | |||||
| if (!$user) { | if (!$user) { | ||||
| throw new Exception('Invalid username.'); | throw new Exception( | ||||
| pht( | |||||
| 'Device request identifies an acting user with an invalid '. | |||||
| 'username ("%s"). There is no user with this username.', | |||||
| $act_as_name)); | |||||
| } | |||||
| } | } | ||||
| $ssh_log->setData( | $ssh_log->setData( | ||||
| array( | array( | ||||
| 'u' => $user->getUsername(), | 'u' => $user->getUsername(), | ||||
| 'P' => $user->getPHID(), | 'P' => $user->getPHID(), | ||||
| )); | )); | ||||
| if (!$user->isUserActivated()) { | if (!$user->isUserActivated()) { | ||||
| throw new Exception(pht('Your account is not activated.')); | throw new Exception( | ||||
| } | pht( | ||||
| 'Your account ("%s") is not activated. Visit the web interface '. | |||||
| if ($args->getArg('ssh-command')) { | 'for more information.', | ||||
| $original_command = $args->getArg('ssh-command'); | $user->getUsername())); | ||||
| } else { | |||||
| $original_command = getenv('SSH_ORIGINAL_COMMAND'); | |||||
| } | } | ||||
| $workflows = id(new PhutilSymbolLoader()) | $workflows = id(new PhutilSymbolLoader()) | ||||
| ->setAncestorClass('PhabricatorSSHWorkflow') | ->setAncestorClass('PhabricatorSSHWorkflow') | ||||
| ->loadObjects(); | ->loadObjects(); | ||||
| $workflow_names = mpull($workflows, 'getName', 'getName'); | $workflow_names = mpull($workflows, 'getName', 'getName'); | ||||
| // Now, rebuild the original command. | |||||
| $original_argv = id(new PhutilShellLexer()) | |||||
| ->splitArguments($original_command); | |||||
| if (!$original_argv) { | if (!$original_argv) { | ||||
| throw new Exception( | throw new Exception( | ||||
| pht( | pht( | ||||
| "Welcome to Phabricator.\n\n". | "Welcome to Phabricator.\n\n". | ||||
| "You are logged in as %s.\n\n". | "You are logged in as %s.\n\n". | ||||
| "You haven't specified a command to run. This means you're requesting ". | "You haven't specified a command to run. This means you're requesting ". | ||||
| "an interactive shell, but Phabricator does not provide an ". | "an interactive shell, but Phabricator does not provide an ". | ||||
| "interactive shell over SSH.\n\n". | "interactive shell over SSH.\n\n". | ||||
| "Usually, you should run a command like `git clone` or `hg push` ". | "Usually, you should run a command like `git clone` or `hg push` ". | ||||
| "rather than connecting directly with SSH.\n\n". | "rather than connecting directly with SSH.\n\n". | ||||
| "Supported commands are: %s.", | "Supported commands are: %s.", | ||||
| $user->getUsername(), | $user->getUsername(), | ||||
| implode(', ', $workflow_names))); | implode(', ', $workflow_names))); | ||||
| } | } | ||||
| $log_argv = implode(' ', array_slice($original_argv, 1)); | $log_argv = implode(' ', $original_argv); | ||||
Not Done Inline ActionsThis fixes a bug, stripping was incorrect here and we were logging without the "hg" / "git" / "svn" bit. epriestley: This fixes a bug, stripping was incorrect here and we were logging without the "hg" / "git" /… | |||||
| $log_argv = id(new PhutilUTF8StringTruncator()) | $log_argv = id(new PhutilUTF8StringTruncator()) | ||||
| ->setMaximumCodepoints(128) | ->setMaximumCodepoints(128) | ||||
| ->truncateString($log_argv); | ->truncateString($log_argv); | ||||
| $ssh_log->setData( | $ssh_log->setData( | ||||
| array( | array( | ||||
| 'C' => $original_argv[0], | 'C' => $original_argv[0], | ||||
| 'U' => $log_argv, | 'U' => $log_argv, | ||||
| )); | )); | ||||
| $command = head($original_argv); | $command = head($original_argv); | ||||
| array_unshift($original_argv, 'phabricator-ssh-exec'); | |||||
| $original_args = new PhutilArgumentParser($original_argv); | $parseable_argv = $original_argv; | ||||
| array_unshift($parseable_argv, 'phabricator-ssh-exec'); | |||||
| $parsed_args = new PhutilArgumentParser($parseable_argv); | |||||
| if (empty($workflow_names[$command])) { | if (empty($workflow_names[$command])) { | ||||
| throw new Exception('Invalid command.'); | throw new Exception('Invalid command.'); | ||||
| } | } | ||||
| $workflow = $original_args->parseWorkflows($workflows); | $workflow = $parsed_args->parseWorkflows($workflows); | ||||
| $workflow->setUser($user); | $workflow->setUser($user); | ||||
| $workflow->setOriginalArguments($original_argv); | |||||
| $workflow->setIsClusterRequest($is_cluster_request); | |||||
| $sock_stdin = fopen('php://stdin', 'r'); | $sock_stdin = fopen('php://stdin', 'r'); | ||||
| if (!$sock_stdin) { | if (!$sock_stdin) { | ||||
| throw new Exception('Unable to open stdin.'); | throw new Exception('Unable to open stdin.'); | ||||
| } | } | ||||
| $sock_stdout = fopen('php://stdout', 'w'); | $sock_stdout = fopen('php://stdout', 'w'); | ||||
| if (!$sock_stdout) { | if (!$sock_stdout) { | ||||
| Show All 10 Lines | $socket_channel = new PhutilSocketChannel( | ||||
| $sock_stdout); | $sock_stdout); | ||||
| $error_channel = new PhutilSocketChannel(null, $sock_stderr); | $error_channel = new PhutilSocketChannel(null, $sock_stderr); | ||||
| $metrics_channel = new PhutilMetricsChannel($socket_channel); | $metrics_channel = new PhutilMetricsChannel($socket_channel); | ||||
| $workflow->setIOChannel($metrics_channel); | $workflow->setIOChannel($metrics_channel); | ||||
| $workflow->setErrorChannel($error_channel); | $workflow->setErrorChannel($error_channel); | ||||
| $rethrow = null; | $rethrow = null; | ||||
| try { | try { | ||||
| $err = $workflow->execute($original_args); | $err = $workflow->execute($parsed_args); | ||||
| $metrics_channel->flush(); | $metrics_channel->flush(); | ||||
| $error_channel->flush(); | $error_channel->flush(); | ||||
| } catch (Exception $ex) { | } catch (Exception $ex) { | ||||
| $rethrow = $ex; | $rethrow = $ex; | ||||
| } | } | ||||
| // Always write this if we got as far as building a metrics channel. | // Always write this if we got as far as building a metrics channel. | ||||
| Show All 21 Lines | |||||
These variables are junk since we have another $user and a $user_name vs $username, I'll clean this up.