@recaptime-dev's working patches + fork for Phorge, a community fork of Phabricator. (Upstream dev and stable branches are at upstream/main and upstream/stable respectively.) hq.recaptime.dev/wiki/Phorge
phorge phabricator
at recaptime-dev/main 349 lines 10 kB view raw
1#!/usr/bin/env php 2<?php 3 4$ssh_start_time = microtime(true); 5 6$root = dirname(dirname(dirname(__FILE__))); 7require_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(pht(<<<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 30EOSYNOPSIS 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 69try { 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 (phutil_nonempty_string($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 (phutil_nonempty_string($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::class) 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 349exit($err);