diff --git a/bin/ssh-auth-key b/bin/ssh-auth-key new file mode 120000 index 0000000000..7dff83c316 --- /dev/null +++ b/bin/ssh-auth-key @@ -0,0 +1 @@ +../scripts/ssh/ssh-auth-key.php \ No newline at end of file diff --git a/resources/sshd/phabricator-ssh-hook.sh b/resources/sshd/phabricator-ssh-hook.sh new file mode 100755 index 0000000000..e405729cef --- /dev/null +++ b/resources/sshd/phabricator-ssh-hook.sh @@ -0,0 +1,8 @@ +#!/bin/sh + +### +### WARNING: This feature is new and experimental. Use it at your own risk! +### + +ROOT=/INSECURE/devtools/phabricator +exec "$ROOT/bin/ssh-auth" $@ diff --git a/resources/sshd/sshd_config.example b/resources/sshd/sshd_config.example new file mode 100644 index 0000000000..da3897fc8e --- /dev/null +++ b/resources/sshd/sshd_config.example @@ -0,0 +1,24 @@ +### +### WARNING: This feature is new and experimental. Use it at your own risk! +### + +# You must have OpenSSHD 6.2 or newer; support for AuthorizedKeysCommand was +# added in this version. + +Port 2222 +AuthorizedKeysCommand /etc/phabricator-ssh-hook.sh +AuthorizedKeysCommandUser some-unprivileged-user + +# You may need to tweak these options, but mostly they just turn off everything +# dangerous. + +Protocol 2 +PermitRootLogin no +AllowAgentForwarding no +AllowTcpForwarding no +PrintMotd no +PrintLastLog no +PasswordAuthentication no +AuthorizedKeysFile none + +PidFile /var/run/sshd-phabricator.pid diff --git a/scripts/ssh/ssh-auth.php b/scripts/ssh/ssh-auth-key.php similarity index 96% copy from scripts/ssh/ssh-auth.php copy to scripts/ssh/ssh-auth-key.php index 96e6ef389b..362e8d1be4 100755 --- a/scripts/ssh/ssh-auth.php +++ b/scripts/ssh/ssh-auth-key.php @@ -1,61 +1,61 @@ #!/usr/bin/env php <?php $root = dirname(dirname(dirname(__FILE__))); require_once $root.'/scripts/__init_script__.php'; $cert = file_get_contents('php://stdin'); if (!$cert) { exit(1); } $parts = preg_split('/\s+/', $cert); if (count($parts) < 2) { exit(1); } list($type, $body) = $parts; $user_dao = new PhabricatorUser(); $ssh_dao = new PhabricatorUserSSHKey(); $conn_r = $user_dao->establishConnection('r'); $row = queryfx_one( $conn_r, 'SELECT userName FROM %T u JOIN %T ssh ON u.phid = ssh.userPHID WHERE ssh.keyType = %s AND ssh.keyBody = %s', $user_dao->getTableName(), $ssh_dao->getTableName(), $type, $body); if (!$row) { exit(1); } $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); +$cmd = addcslashes($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-auth.php b/scripts/ssh/ssh-auth.php index 96e6ef389b..dc7b4d098e 100755 --- a/scripts/ssh/ssh-auth.php +++ b/scripts/ssh/ssh-auth.php @@ -1,61 +1,48 @@ #!/usr/bin/env php <?php $root = dirname(dirname(dirname(__FILE__))); require_once $root.'/scripts/__init_script__.php'; -$cert = file_get_contents('php://stdin'); - -if (!$cert) { - exit(1); -} - -$parts = preg_split('/\s+/', $cert); -if (count($parts) < 2) { - exit(1); -} - -list($type, $body) = $parts; - $user_dao = new PhabricatorUser(); $ssh_dao = new PhabricatorUserSSHKey(); $conn_r = $user_dao->establishConnection('r'); -$row = queryfx_one( +$rows = queryfx_all( $conn_r, - 'SELECT userName FROM %T u JOIN %T ssh ON u.phid = ssh.userPHID - WHERE ssh.keyType = %s AND ssh.keyBody = %s', + 'SELECT userName, keyBody, keyType FROM %T u JOIN %T ssh + ON u.phid = ssh.userPHID', $user_dao->getTableName(), - $ssh_dao->getTableName(), - $type, - $body); + $ssh_dao->getTableName()); -if (!$row) { - exit(1); -} +$bin = $root.'/bin/ssh-exec'; +foreach ($rows as $row) { + $user = $row['userName']; -$user = idx($row, 'userName'); + $cmd = csprintf('%s --phabricator-ssh-user %s', $bin, $user); + // This is additional escaping for the SSH 'command="..."' string. + $cmd = addcslashes($cmd, '"\\'); -if (!$user) { - exit(1); -} + // Strip out newlines and other nonsense from the key type and key body. + + $type = $row['keyType']; + $type = preg_replace('@[\x00-\x20]+@', '', $type); + + $key = $row['keyBody']; + $key = preg_replace('@[\x00-\x20]+@', '', $key); -if (!PhabricatorUser::validateUsername($user)) { - exit(1); + + $options = array( + 'command="'.$cmd.'"', + 'no-port-forwarding', + 'no-X11-forwarding', + 'no-agent-forwarding', + 'no-pty', + ); + $options = implode(',', $options); + + $lines[] = $options.' '.$type.' '.$key."\n"; } -$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); +echo implode('', $lines); exit(0); diff --git a/scripts/ssh/ssh-exec.php b/scripts/ssh/ssh-exec.php index a9c639f75d..6bd5b71ef3 100755 --- a/scripts/ssh/ssh-exec.php +++ b/scripts/ssh/ssh-exec.php @@ -1,91 +1,96 @@ #!/usr/bin/env php <?php $root = dirname(dirname(dirname(__FILE__))); require_once $root.'/scripts/__init_script__.php'; -$original_command = getenv('SSH_ORIGINAL_COMMAND'); -$original_argv = id(new PhutilShellLexer())->splitArguments($original_command); -$argv = array_merge($argv, $original_argv); - +// First, figure out the authenticated user. $args = new PhutilArgumentParser($argv); $args->setTagline('receive SSH requests'); $args->setSynopsis(<<<EOSYNOPSIS -**ssh-exec** --phabricator-ssh-user __user__ __commmand__ [__options__] +**ssh-exec** --phabricator-ssh-user __user__ [--ssh-command __commmand__] Receive SSH requests. - EOSYNOPSIS ); -// NOTE: Do NOT parse standard arguments. Arguments are coming from a remote -// client over SSH, and they should not be able to execute "--xprofile", -// "--recon", etc. - -$args->parsePartial( +$args->parse( array( array( 'name' => 'phabricator-ssh-user', 'param' => 'username', ), + array( + 'name' => 'ssh-command', + 'param' => 'command', + ), )); 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."); } + if ($args->getArg('ssh-command')) { + $original_command = $args->getArg('ssh-command'); + } else { + $original_command = getenv('SSH_ORIGINAL_COMMAND'); + } + + // Now, rebuild the original command. + $original_argv = id(new PhutilShellLexer()) + ->splitArguments($original_command); + if (!$original_argv) { + throw new Exception("No interactive logins."); + } + $command = head($original_argv); + array_unshift($original_argv, 'phabricator-ssh-exec'); + + $original_args = new PhutilArgumentParser($original_argv); + $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 interactive logins."); - } else { - $command = head($remain); - $workflow_names = mpull($workflows, 'getName', 'getName'); - if (empty($workflow_names[$command])) { - throw new Exception("Invalid command."); - } + $workflow_names = mpull($workflows, 'getName', 'getName'); + if (empty($workflow_names[$command])) { + throw new Exception("Invalid command."); } - $workflow = $args->parseWorkflows($workflows); + $workflow = $original_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); + $err = $workflow->execute($original_args); $metrics_channel->flush(); } catch (Exception $ex) { echo "phabricator-ssh-exec: ".$ex->getMessage()."\n"; exit(1); } diff --git a/src/applications/conduit/ssh/ConduitSSHWorkflow.php b/src/applications/conduit/ssh/ConduitSSHWorkflow.php index ba456efaca..74221b51dc 100644 --- a/src/applications/conduit/ssh/ConduitSSHWorkflow.php +++ b/src/applications/conduit/ssh/ConduitSSHWorkflow.php @@ -1,83 +1,83 @@ <?php final class ConduitSSHWorkflow extends PhabricatorSSHWorkflow { public function didConstruct() { $this->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 = idx($raw_params, 'params', array()); + $params = idx($raw_params, 'params', '[]'); $params = json_decode($params, true); $metadata = idx($params, '__conduit__', array()); unset($params['__conduit__']); $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 = id(new PhabricatorConduitMethodCallLog()) ->setCallerPHID($this->getUser()->getPHID()) ->setConnectionID($connection_id) ->setMethod($method) ->setError((string)$error_code) ->setDuration(1000000 * ($time_end - $time_start)) ->save(); } }