4 $ssh_start_time = microtime(true);
6 $root = dirname(dirname(dirname(__FILE__
)));
7 require_once $root.'/scripts/init/init-script.php';
9 $error_log = id(new PhutilErrorLog())
10 ->setLogName(pht('SSH Error Log'))
11 ->setLogPath(PhabricatorEnv
::getEnvConfig('log.ssh-error.path'))
14 $ssh_log = PhabricatorSSHLog
::getLog();
16 $request_identifier = Filesystem
::readRandomCharacters(12);
19 'Q' => $request_identifier,
22 $args = new PhutilArgumentParser($argv);
23 $args->setTagline(pht('execute SSH requests'));
24 $args->setSynopsis(<<<EOSYNOPSIS
25 **ssh-exec** --phabricator-ssh-user __user__ [--ssh-command __commmand__]
26 **ssh-exec** --phabricator-ssh-device __device__ [--ssh-command __commmand__]
27 Execute authenticated SSH requests. This script is normally invoked
28 via SSHD, but can be invoked manually for testing.
33 $args->parseStandardArguments();
37 'name' => 'phabricator-ssh-user',
38 'param' => 'username',
40 'If the request authenticated with a user key, the name of the '.
44 'name' => 'phabricator-ssh-device',
47 'If the request authenticated with a device key, the name of the '.
51 'name' => 'phabricator-ssh-key',
54 'The ID of the SSH key which authenticated this request. This is '.
55 'used to allow logs to report when specific keys were used, to make '.
56 'it easier to manage credentials.'),
59 'name' => 'ssh-command',
62 'Provide a command to execute. This makes testing this script '.
63 'easier. When running normally, the command is read from the '.
64 'environment (%s), which is populated by sshd.',
65 'SSH_ORIGINAL_COMMAND'),
70 $remote_address = null;
71 $ssh_client = getenv('SSH_CLIENT');
73 // This has the format "<ip> <remote-port> <local-port>". Grab the IP.
74 $remote_address = head(explode(' ', $ssh_client));
77 'r' => $remote_address,
81 $key_id = $args->getArg('phabricator-ssh-key');
89 $user_name = $args->getArg('phabricator-ssh-user');
90 $device_name = $args->getArg('phabricator-ssh-device');
94 $is_cluster_request = false;
96 if ($user_name && $device_name) {
99 'The %s and %s flags are mutually exclusive. You can not '.
100 'authenticate as both a user ("%s") and a device ("%s"). '.
101 'Specify one or the other, but not both.',
102 '--phabricator-ssh-user',
103 '--phabricator-ssh-device',
106 } else if ($user_name !== null && strlen($user_name)) {
107 $user = id(new PhabricatorPeopleQuery())
108 ->setViewer(PhabricatorUser
::getOmnipotentUser())
109 ->withUsernames(array($user_name))
114 'Invalid username ("%s"). There is no user with this username.',
118 id(new PhabricatorAuthSessionEngine())
119 ->willServeRequestForUser($user);
120 } else if ($device_name !== null && strlen($device_name)) {
121 if (!$remote_address) {
124 'Unable to identify remote address from the %s environment '.
125 'variable. Device authentication is accepted only from trusted '.
130 if (!PhabricatorEnv
::isClusterAddress($remote_address)) {
133 'This request originates from outside of the cluster address range. '.
134 'Requests signed with a trusted device key must originate from '.
138 $device = id(new AlmanacDeviceQuery())
139 ->setViewer(PhabricatorUser
::getOmnipotentUser())
140 ->withNames(array($device_name))
145 'Invalid device name ("%s"). There is no device with this name.',
149 if ($device->isDisabled()) {
152 'This request has authenticated as a device ("%s"), but this '.
153 'device is disabled.',
154 $device->getName()));
157 // We're authenticated as a device, but we're going to read the user out of
158 // the command below.
159 $is_cluster_request = true;
163 'This script must be invoked with either the %s or %s flag.',
164 '--phabricator-ssh-user',
165 '--phabricator-ssh-device'));
168 if ($args->getArg('ssh-command')) {
169 $original_command = $args->getArg('ssh-command');
171 $original_command = getenv('SSH_ORIGINAL_COMMAND');
174 $original_argv = id(new PhutilShellLexer())
175 ->splitArguments($original_command);
178 // If we're authenticating as a device, the first argument may be a
179 // "@username" argument to act as a particular user.
180 $first_argument = head($original_argv);
181 if (preg_match('/^@/', $first_argument)) {
182 $act_as_name = array_shift($original_argv);
183 $act_as_name = substr($act_as_name, 1);
184 $user = id(new PhabricatorPeopleQuery())
185 ->setViewer(PhabricatorUser
::getOmnipotentUser())
186 ->withUsernames(array($act_as_name))
191 'Device request identifies an acting user with an invalid '.
192 'username ("%s"). There is no user with this username.',
196 $user = PhabricatorUser
::getOmnipotentUser();
200 if ($user->isOmnipotent()) {
201 $user_name = 'device/'.$device->getName();
203 $user_name = $user->getUsername();
209 'P' => $user->getPHID(),
213 if (!$user->canEstablishSSHSessions()) {
216 'Your account ("%s") does not have permission to establish SSH '.
217 'sessions. Visit the web interface for more information.',
222 $workflows = id(new PhutilClassMapQuery())
223 ->setAncestorClass('PhabricatorSSHWorkflow')
224 ->setUniqueMethod('getName')
227 $command_list = array_keys($workflows);
228 $command_list = implode(', ', $command_list);
230 $error_lines = array();
231 $error_lines[] = pht(
233 PlatformSymbols
::getPlatformServerName());
234 $error_lines[] = pht(
235 'You are logged in as %s.',
238 if (!$original_argv) {
239 $error_lines[] = pht(
240 'You have not specified a command to run. This means you are requesting '.
241 'an interactive shell, but this server does not provide interactive '.
243 $error_lines[] = pht(
244 '(Usually, you should run a command like "git clone" or "hg push" '.
245 'instead of connecting directly with SSH.)');
246 $error_lines[] = pht(
247 'Supported commands are: %s.',
250 $error_lines = implode("\n\n", $error_lines);
251 throw new PhutilArgumentUsageException($error_lines);
254 $log_argv = implode(' ', $original_argv);
255 $log_argv = id(new PhutilUTF8StringTruncator())
256 ->setMaximumCodepoints(128)
257 ->truncateString($log_argv);
261 'C' => $original_argv[0],
265 $command = head($original_argv);
267 $parseable_argv = $original_argv;
268 array_unshift($parseable_argv, 'phabricator-ssh-exec');
270 $parsed_args = new PhutilArgumentParser($parseable_argv);
272 if (empty($workflows[$command])) {
273 $error_lines[] = pht(
274 'You have specified the command "%s", but that command is not '.
275 'supported by this server. As received by this server, your entire '.
276 'argument list was:',
279 $error_lines[] = csprintf(' $ ssh ... -- %Ls', $parseable_argv);
281 $error_lines[] = pht(
282 'Supported commands are: %s.',
285 $error_lines = implode("\n\n", $error_lines);
286 throw new PhutilArgumentUsageException($error_lines);
289 $workflow = $parsed_args->parseWorkflows($workflows);
290 $workflow->setSSHUser($user);
291 $workflow->setOriginalArguments($original_argv);
292 $workflow->setIsClusterRequest($is_cluster_request);
293 $workflow->setRequestIdentifier($request_identifier);
295 $sock_stdin = fopen('php://stdin', 'r');
297 throw new Exception(pht('Unable to open stdin.'));
300 $sock_stdout = fopen('php://stdout', 'w');
302 throw new Exception(pht('Unable to open stdout.'));
305 $sock_stderr = fopen('php://stderr', 'w');
307 throw new Exception(pht('Unable to open stderr.'));
310 $socket_channel = new PhutilSocketChannel(
313 $error_channel = new PhutilSocketChannel(null, $sock_stderr);
314 $metrics_channel = new PhutilMetricsChannel($socket_channel);
315 $workflow->setIOChannel($metrics_channel);
316 $workflow->setErrorChannel($error_channel);
320 $err = $workflow->execute($parsed_args);
322 $metrics_channel->flush();
323 $error_channel->flush();
324 } catch (Exception
$ex) {
328 // Always write this if we got as far as building a metrics channel.
331 'i' => $metrics_channel->getBytesRead(),
332 'o' => $metrics_channel->getBytesWritten(),
338 } catch (Exception
$ex) {
339 fwrite(STDERR
, "phabricator-ssh-exec: ".$ex->getMessage()."\n");
346 'T' => phutil_microseconds_since($ssh_start_time),