Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
freebsd
GitHub Repository: freebsd/phabricator
Path: blob/master/scripts/ssh/ssh-exec.php
12241 views
1
#!/usr/bin/env php
2
<?php
3
4
$ssh_start_time = microtime(true);
5
6
$root = dirname(dirname(dirname(__FILE__)));
7
require_once $root.'/scripts/init/init-script.php';
8
9
$error_log = id(new PhutilErrorLog())
10
->setLogName(pht('SSH Error Log'))
11
->setLogPath(PhabricatorEnv::getEnvConfig('log.ssh-error.path'))
12
->activateLog();
13
14
$ssh_log = PhabricatorSSHLog::getLog();
15
16
$request_identifier = Filesystem::readRandomCharacters(12);
17
$ssh_log->setData(
18
array(
19
'Q' => $request_identifier,
20
));
21
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.
29
30
EOSYNOPSIS
31
);
32
33
$args->parseStandardArguments();
34
$args->parse(
35
array(
36
array(
37
'name' => 'phabricator-ssh-user',
38
'param' => 'username',
39
'help' => pht(
40
'If the request authenticated with a user key, the name of the '.
41
'user.'),
42
),
43
array(
44
'name' => 'phabricator-ssh-device',
45
'param' => 'name',
46
'help' => pht(
47
'If the request authenticated with a device key, the name of the '.
48
'device.'),
49
),
50
array(
51
'name' => 'phabricator-ssh-key',
52
'param' => 'id',
53
'help' => pht(
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.'),
57
),
58
array(
59
'name' => 'ssh-command',
60
'param' => 'command',
61
'help' => pht(
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'),
66
),
67
));
68
69
try {
70
$remote_address = null;
71
$ssh_client = getenv('SSH_CLIENT');
72
if ($ssh_client) {
73
// This has the format "<ip> <remote-port> <local-port>". Grab the IP.
74
$remote_address = head(explode(' ', $ssh_client));
75
$ssh_log->setData(
76
array(
77
'r' => $remote_address,
78
));
79
}
80
81
$key_id = $args->getArg('phabricator-ssh-key');
82
if ($key_id) {
83
$ssh_log->setData(
84
array(
85
'k' => $key_id,
86
));
87
}
88
89
$user_name = $args->getArg('phabricator-ssh-user');
90
$device_name = $args->getArg('phabricator-ssh-device');
91
92
$user = null;
93
$device = null;
94
$is_cluster_request = false;
95
96
if ($user_name && $device_name) {
97
throw new Exception(
98
pht(
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',
104
$user_name,
105
$device_name));
106
} else if ($user_name !== null && strlen($user_name)) {
107
$user = id(new PhabricatorPeopleQuery())
108
->setViewer(PhabricatorUser::getOmnipotentUser())
109
->withUsernames(array($user_name))
110
->executeOne();
111
if (!$user) {
112
throw new Exception(
113
pht(
114
'Invalid username ("%s"). There is no user with this username.',
115
$user_name));
116
}
117
118
id(new PhabricatorAuthSessionEngine())
119
->willServeRequestForUser($user);
120
} else if ($device_name !== null && strlen($device_name)) {
121
if (!$remote_address) {
122
throw new Exception(
123
pht(
124
'Unable to identify remote address from the %s environment '.
125
'variable. Device authentication is accepted only from trusted '.
126
'sources.',
127
'SSH_CLIENT'));
128
}
129
130
if (!PhabricatorEnv::isClusterAddress($remote_address)) {
131
throw new Exception(
132
pht(
133
'This request originates from outside of the cluster address range. '.
134
'Requests signed with a trusted device key must originate from '.
135
'trusted hosts.'));
136
}
137
138
$device = id(new AlmanacDeviceQuery())
139
->setViewer(PhabricatorUser::getOmnipotentUser())
140
->withNames(array($device_name))
141
->executeOne();
142
if (!$device) {
143
throw new Exception(
144
pht(
145
'Invalid device name ("%s"). There is no device with this name.',
146
$device_name));
147
}
148
149
if ($device->isDisabled()) {
150
throw new Exception(
151
pht(
152
'This request has authenticated as a device ("%s"), but this '.
153
'device is disabled.',
154
$device->getName()));
155
}
156
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;
160
} else {
161
throw new Exception(
162
pht(
163
'This script must be invoked with either the %s or %s flag.',
164
'--phabricator-ssh-user',
165
'--phabricator-ssh-device'));
166
}
167
168
if ($args->getArg('ssh-command')) {
169
$original_command = $args->getArg('ssh-command');
170
} else {
171
$original_command = getenv('SSH_ORIGINAL_COMMAND');
172
}
173
174
$original_argv = id(new PhutilShellLexer())
175
->splitArguments($original_command);
176
177
if ($device) {
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))
187
->executeOne();
188
if (!$user) {
189
throw new Exception(
190
pht(
191
'Device request identifies an acting user with an invalid '.
192
'username ("%s"). There is no user with this username.',
193
$act_as_name));
194
}
195
} else {
196
$user = PhabricatorUser::getOmnipotentUser();
197
}
198
}
199
200
if ($user->isOmnipotent()) {
201
$user_name = 'device/'.$device->getName();
202
} else {
203
$user_name = $user->getUsername();
204
}
205
206
$ssh_log->setData(
207
array(
208
'u' => $user_name,
209
'P' => $user->getPHID(),
210
));
211
212
if (!$device) {
213
if (!$user->canEstablishSSHSessions()) {
214
throw new Exception(
215
pht(
216
'Your account ("%s") does not have permission to establish SSH '.
217
'sessions. Visit the web interface for more information.',
218
$user_name));
219
}
220
}
221
222
$workflows = id(new PhutilClassMapQuery())
223
->setAncestorClass('PhabricatorSSHWorkflow')
224
->setUniqueMethod('getName')
225
->execute();
226
227
$command_list = array_keys($workflows);
228
$command_list = implode(', ', $command_list);
229
230
$error_lines = array();
231
$error_lines[] = pht(
232
'Welcome to %s.',
233
PlatformSymbols::getPlatformServerName());
234
$error_lines[] = pht(
235
'You are logged in as %s.',
236
$user_name);
237
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 '.
242
'shells over SSH.');
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.',
248
$command_list);
249
250
$error_lines = implode("\n\n", $error_lines);
251
throw new PhutilArgumentUsageException($error_lines);
252
}
253
254
$log_argv = implode(' ', $original_argv);
255
$log_argv = id(new PhutilUTF8StringTruncator())
256
->setMaximumCodepoints(128)
257
->truncateString($log_argv);
258
259
$ssh_log->setData(
260
array(
261
'C' => $original_argv[0],
262
'U' => $log_argv,
263
));
264
265
$command = head($original_argv);
266
267
$parseable_argv = $original_argv;
268
array_unshift($parseable_argv, 'phabricator-ssh-exec');
269
270
$parsed_args = new PhutilArgumentParser($parseable_argv);
271
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:',
277
$command);
278
279
$error_lines[] = csprintf(' $ ssh ... -- %Ls', $parseable_argv);
280
281
$error_lines[] = pht(
282
'Supported commands are: %s.',
283
$command_list);
284
285
$error_lines = implode("\n\n", $error_lines);
286
throw new PhutilArgumentUsageException($error_lines);
287
}
288
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);
294
295
$sock_stdin = fopen('php://stdin', 'r');
296
if (!$sock_stdin) {
297
throw new Exception(pht('Unable to open stdin.'));
298
}
299
300
$sock_stdout = fopen('php://stdout', 'w');
301
if (!$sock_stdout) {
302
throw new Exception(pht('Unable to open stdout.'));
303
}
304
305
$sock_stderr = fopen('php://stderr', 'w');
306
if (!$sock_stderr) {
307
throw new Exception(pht('Unable to open stderr.'));
308
}
309
310
$socket_channel = new PhutilSocketChannel(
311
$sock_stdin,
312
$sock_stdout);
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);
317
318
$rethrow = null;
319
try {
320
$err = $workflow->execute($parsed_args);
321
322
$metrics_channel->flush();
323
$error_channel->flush();
324
} catch (Exception $ex) {
325
$rethrow = $ex;
326
}
327
328
// Always write this if we got as far as building a metrics channel.
329
$ssh_log->setData(
330
array(
331
'i' => $metrics_channel->getBytesRead(),
332
'o' => $metrics_channel->getBytesWritten(),
333
));
334
335
if ($rethrow) {
336
throw $rethrow;
337
}
338
} catch (Exception $ex) {
339
fwrite(STDERR, "phabricator-ssh-exec: ".$ex->getMessage()."\n");
340
$err = 1;
341
}
342
343
$ssh_log->setData(
344
array(
345
'c' => $err,
346
'T' => phutil_microseconds_since($ssh_start_time),
347
));
348
349
exit($err);
350
351